Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,72 @@ agent "<identity>" {
}
```

## Experimental read-only plans

The experimental `st2 plan` surface implements only the inspectable file model from the
[st2 plans sketch at revision `5c1d142`](https://gist.github.com/myobie/d5ecfac24cd3965e095a5031cd2e00cb/5c1d1427c0556d95d13890e5c5086cd85b25d994).
The implemented agent integration uses the existing Resource envelope; plan truth stays only in the
linked `plan.kdl`.
An agent links a plan with the existing Agent Spec Resource envelope:

```kdl
agent "app-web" {
resource "receipt-report" _tag="plan" uri="file:plans/receipt-report/plan.kdl"
}
```

The Resource name is the agent-local role. The `_tag` selects the experimental plan reader. The
`uri` resolves from the agent KDL file and must remain inside the selected catalog. The Resource
has no children and owns no plan fields. It may add the agent identity to `referencedBy`, but the
target `plan.kdl` remains the only plan authority.

A plan can keep version content in referenced Markdown files:

```kdl
plan "ship-remote-approvals" {
owner "app-web"
version "0000" content="file:versions/0000.md"
version "0001" content="file:versions/0001.md" {
parent "0000"
why "Browser proof exposed an approval race."
}
}
```

Or a small plan can keep the complete version intent inline in `plan.kdl`:

```kdl
plan "review-follow-up" {
owner "reviewer"
version "0000" {
intent #"""
Review the accepted corrections.

Done means the corrections are present and independently verified.
"""#
}
}
```

Plan identity and owner are explicit in `plan.kdl`, never derived from a Resource name, directory,
or referring agent. Each version has exactly one `content="file:..."` property or one inline
`intent` child. Content references resolve from `plan.kdl` and remain inside the selected catalog.
Revisions declare one or more `parent` links and a non-empty `why`; root versions have no parent.
The frontier is every version with no child, so concurrent siblings remain visible. The experiment
stores no digest or immutable history proof.

```sh
st2 plan validate --catalog examples/plans
st2 plan list --catalog examples/plans
st2 plan show ship-remote-approvals --catalog examples/plans
st2 plan inspect ship-remote-approvals --catalog examples/plans --json
```

All four commands are read-only. They do not select a current version, write progress, emit events,
execute a plan, reconcile agents, schedule work, interpret claims, or require CAS. Direct KDL and
direct human-to-agent planning remain complete workflows; this experiment must earn any larger
runtime.

st2 provides `CATALOG`, flat native `ST_ROOT`, local `PTY_ROOT`, `ST_AGENT`, and `ST_HOOKS` to the
task. The complete st2-managed overlay is also persisted in PTY metadata, so a manual `pty restart`
retains those values. Declarations should not contain machine-specific install paths.
Expand Down
22 changes: 22 additions & 0 deletions docs/vrs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,28 @@ atomic inbox file → DING attempt → agent reads → archive receipt
- **R10:** Fleet identities are agents. General-purpose identity kinds are
unsupported.

### Experimental read-only plan inspection

`st2 plan validate|list|show|inspect` is a non-executing probe for the
[source plan sketch at revision `5c1d142`](https://gist.github.com/myobie/d5ecfac24cd3965e095a5031cd2e00cb/5c1d1427c0556d95d13890e5c5086cd85b25d994).
The implemented agent integration uses the existing Resource envelope to link
to plan-owned truth.
It reads standalone plans and plans linked through a childless Agent Spec
Resource whose `_tag` is `plan`. The Resource name is an agent-local role. The
Resource can add `referencedBy`; it does not own plan identity, owner, version,
parent, reason, or intent. Those fields remain in the referenced `plan.kdl`.
Each version contains either source-relative Markdown `content` or complete
inline `intent`. Resource URIs resolve from the agent KDL file. Content URIs
resolve from `plan.kdl`. Both stay within the selected catalog. A derived
frontier retains concurrent siblings. The experiment stores no content digest
or immutable history proof.

The parser is deliberately deny-by-default for plan fields beyond that model.
It has no current pointer, controller, execution, scheduling, step graph,
retry, claim, receipt, event, reconciliation, CAS, or mutation behavior. It
does not yet fulfill R08 plan-progress observability or resolve DQ3; direct KDL
and direct human-to-agent planning remain first-class.

The owner updates this spec whenever implementation changes.
Changing [vision.md](./vision.md) or [requirements.md](./requirements.md)
requires Nathan's explicit approval.
Expand Down
11 changes: 11 additions & 0 deletions examples/plans/agent.kdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
agent "app-web" {
host "example"
command "true"
resource "release-plan" _tag="plan" uri="file:ship-remote-approvals/plan.kdl"
}

agent "reviewer" {
host "example"
command "true"
resource "review-plan" _tag="plan" uri="file:review-follow-up/plan.kdl"
}
11 changes: 11 additions & 0 deletions examples/plans/review-follow-up/plan.kdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plan "review-follow-up" {
owner "reviewer"

version "0000" {
intent #"""
Review the accepted corrections.

Done means the corrections are present and independently verified.
"""#
}
}
9 changes: 9 additions & 0 deletions examples/plans/ship-remote-approvals/plan.kdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plan "ship-remote-approvals" {
owner "app-web"

version "0000" content="file:versions/0000.md"
version "0001" content="file:versions/0001.md" {
parent "0000"
why "Browser proof exposed an approval race."
}
}
3 changes: 3 additions & 0 deletions examples/plans/ship-remote-approvals/versions/0000.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ship remote approvals

Done means an authorized reviewer can approve the exact remote change.
3 changes: 3 additions & 0 deletions examples/plans/ship-remote-approvals/versions/0001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ship remote approvals

Done means an authorized reviewer can approve the exact remote change without racing stale state.
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod host_lock;
pub mod isolate;
pub mod materialize;
pub mod message;
pub mod plans;
pub mod pretrust;
pub mod reconcile;
pub mod resource;
Expand Down
220 changes: 220 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ enum Command {
#[arg(long)]
json: bool,
},
/// EXPERIMENTAL, READ-ONLY: parse, validate, and inspect versioned catalog plans.
#[command(subcommand)]
Plan(PlanCmd),
/// Print a shell completion script for `st2` to stdout (`st2 completions <bash|zsh|fish|…>`).
/// Generated from the live command tree, so it never drifts from the actual flags.
Completions {
Expand Down Expand Up @@ -475,6 +478,48 @@ enum HooksCmd {
VerifyOwn,
}

#[derive(Subcommand)]
enum PlanCmd {
/// Validate every Resource-linked or standalone plan without executing or writing anything.
Validate {
/// Catalog folder or KDL file. Prefer --catalog; defaults to the selected catalog.
#[arg(conflicts_with = "catalog_path")]
root: Option<PathBuf>,
/// Emit a machine-readable validation receipt.
#[arg(long)]
json: bool,
},
/// List normalized plan identity, owner, and derived frontier.
List {
/// Catalog folder or KDL file. Prefer --catalog; defaults to the selected catalog.
#[arg(conflicts_with = "catalog_path")]
root: Option<PathBuf>,
/// Emit a machine-readable array.
#[arg(long)]
json: bool,
},
/// Show normalized intent for one explicit plan identity.
Show {
identity: String,
/// Catalog folder or KDL file. Prefer --catalog; defaults to the selected catalog.
#[arg(conflicts_with = "catalog_path")]
root: Option<PathBuf>,
/// Emit machine-readable normalized intent.
#[arg(long)]
json: bool,
},
/// Inspect one plan with source provenance, resolved file paths, and agent references.
Inspect {
identity: String,
/// Catalog folder or KDL file. Prefer --catalog; defaults to the selected catalog.
#[arg(conflicts_with = "catalog_path")]
root: Option<PathBuf>,
/// Emit the complete machine-readable inspection record.
#[arg(long)]
json: bool,
},
}

#[derive(Subcommand)]
enum ResourceCmd {
/// Link a resource (a URL you produced or reference) into your resource list.
Expand Down Expand Up @@ -869,6 +914,7 @@ fn main() -> Result<()> {
let catalog = catalog_arg(None)?;
tasks_cmd(&catalog, host)
}
Command::Plan(command) => plan_cmd(command),
Command::Down { root, host } => {
if root.is_none() && catalog_path.is_none() {
anyhow::bail!(
Expand Down Expand Up @@ -918,6 +964,180 @@ fn main() -> Result<()> {
}
}

fn plan_cmd(command: PlanCmd) -> Result<()> {
match command {
PlanCmd::Validate { root, json } => {
let root = catalog_arg(root)?;
match st2::plans::load(&root) {
Ok(catalog) => {
if json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"result": "valid",
"plans": catalog.plans.len(),
"errors": 0,
}))?
);
} else {
println!(
"valid: {} plan{}; read-only (no execution or writes)",
catalog.plans.len(),
plural(catalog.plans.len())
);
}
Ok(())
}
Err(error) => {
if json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"result": "invalid",
"code": error.code(),
"path": error.path(),
"error": error.to_string(),
}))?
);
}
Err(error.into())
}
}
}
PlanCmd::List { root, json } => {
let catalog = st2::plans::load(&catalog_arg(root)?)?;
if json {
let rows = catalog
.plans
.iter()
.map(|plan| {
serde_json::json!({
"identity": plan.identity,
"owner": plan.owner,
"frontier": plan.frontier,
})
})
.collect::<Vec<_>>();
println!("{}", serde_json::to_string_pretty(&rows)?);
} else {
for plan in catalog.plans {
println!(
"{}\towner={}\tfrontier={}",
plan.identity,
plan.owner,
plan.frontier.join(",")
);
}
}
Ok(())
}
PlanCmd::Show {
identity,
root,
json,
} => {
let catalog = st2::plans::load(&catalog_arg(root)?)?;
let plan = exact_plan(&catalog, &identity)?;
let intent = serde_json::json!({
"identity": plan.identity,
"owner": plan.owner,
"versions": plan.versions.iter().map(|version| {
let mut row = serde_json::json!({
"identity": version.identity,
"parents": version.parents,
"why": version.why,
});
if let Some(content) = &version.content {
row["content"] = serde_json::json!(content);
}
if let Some(intent) = &version.intent {
row["intent"] = serde_json::json!(intent);
}
row
}).collect::<Vec<_>>(),
"frontier": plan.frontier,
});
if json {
println!("{}", serde_json::to_string_pretty(&intent)?);
} else {
println!("plan {} owner={}", plan.identity, plan.owner);
for version in &plan.versions {
let marker = if plan.frontier.contains(&version.identity) {
" [frontier]"
} else {
""
};
if let Some(content) = &version.content {
println!(" version {}{marker} content={content}", version.identity);
} else if let Some(intent) = &version.intent {
println!(
" version {}{marker} intent={}",
version.identity,
serde_json::to_string(intent)?
);
}
if !version.parents.is_empty() {
println!(" parents: {}", version.parents.join(", "));
}
if let Some(why) = &version.why {
println!(" why: {why}");
}
}
}
Ok(())
}
PlanCmd::Inspect {
identity,
root,
json,
} => {
let catalog = st2::plans::load(&catalog_arg(root)?)?;
let plan = exact_plan(&catalog, &identity)?;
if json {
println!("{}", serde_json::to_string_pretty(plan)?);
} else {
println!(
"plan {} owner={} kind={:?}\n source: {}\n referenced-by: {}\n frontier: {}",
plan.identity,
plan.owner,
plan.source_kind,
plan.source.display(),
plan.referenced_by.join(","),
plan.frontier.join(",")
);
for version in &plan.versions {
if let (Some(content), Some(resolved)) =
(&version.content, &version.resolved_content)
{
println!(
" {}: {content} -> {}",
version.identity,
resolved.display()
);
} else if let Some(intent) = &version.intent {
println!(
" {}: inline intent={}",
version.identity,
serde_json::to_string(intent)?
);
}
}
}
Ok(())
}
}
}

fn exact_plan<'a>(
catalog: &'a st2::plans::PlanCatalog,
identity: &str,
) -> Result<&'a st2::plans::Plan> {
catalog
.plans
.iter()
.find(|plan| plan.identity == identity)
.with_context(|| format!("no plan '{identity}' found"))
}
fn hooks_cmd(command: HooksCmd) -> Result<()> {
match command {
HooksCmd::Install {
Expand Down
Loading
Loading