From 83ce49cc299f0d200b02754df34284569f33d952 Mon Sep 17 00:00:00 2001 From: Yuto Nishida Date: Sun, 11 Oct 2026 15:07:30 -0700 Subject: [PATCH] silverwood: model subcommands as choices with group hints --- experimental/silverwood/CLI_SPEC.jsonnet | 684 +++++++++--------- experimental/silverwood/DESIGN.md | 6 +- .../silverwood-cli/src/contract_test.rs | 30 +- .../crates/silverwood-cli/src/main.rs | 2 + 4 files changed, 383 insertions(+), 339 deletions(-) diff --git a/experimental/silverwood/CLI_SPEC.jsonnet b/experimental/silverwood/CLI_SPEC.jsonnet index 6135204..ed96cac 100644 --- a/experimental/silverwood/CLI_SPEC.jsonnet +++ b/experimental/silverwood/CLI_SPEC.jsonnet @@ -1,7 +1,7 @@ // Human-edited algebraic contract for the silverwood CLI. -// A command's options and positionals form a product; subcommands form a sum. +// A command's options and positionals form a product; subcommands.choices form a sum. // Options apply at the declaring command and every descendant. -// Missing child lists are empty; hidden and option required default to false. +// Missing child lists, including subcommands.choices, are empty; hidden and option required default to false. // Positionals default to required. A domain marks a one-value option; without it, // an option is a zero-argument flag. Hints may be omitted when they add no intent. // Validator URIs express intent; the conformance check does not inspect Rust validator wiring. @@ -20,220 +20,290 @@ }, { longName: "json", generatedDescriptionHint: "Emit machine-readable JSON instead of human-readable text." }, ], - subcommands: [ - { name: "info", generatedDescriptionHint: "Open the forest and print its identity." }, - { - name: "new", - intentHint: "Make the user choose a workstream kind and supply a human name.", - generatedDescriptionHint: "Create a workstream.", - options: [ - { longName: "name", generatedDescriptionHint: "Human-friendly workstream name.", domain: { semanticName: "NAME" } }, - ], - subcommands: [ - { - name: "basic", - intentHint: "Make checkout timing and mode explicit before creating a managed code checkout.", - generatedDescriptionHint: "Create a basic workstream using a checkout mode.", - options: [ + subcommands: { + choices: [ + { name: "info", generatedDescriptionHint: "Open the forest and print its identity." }, + { + name: "new", + intentHint: "Make the user choose a workstream kind and supply a human name.", + generatedDescriptionHint: "Create a workstream.", + options: [ + { longName: "name", generatedDescriptionHint: "Human-friendly workstream name.", domain: { semanticName: "NAME" } }, + ], + subcommands: { + intentHint: "Choose the kind of workstream to create.", + generatedDescriptionHint: "Available workstream kinds.", + choices: [ { - longName: "checkout-extent", - generatedDescriptionHint: "Provision now or defer checkout until later.", - domain: { semanticName: "CHECKOUT_EXTENT", extensionalDef: ["full", "skip"] }, - required: true, - }, - ], - subcommands: [ - { - name: "jj-colocated", - generatedDescriptionHint: "Clone an HTTPS Git endpoint with a colocated jj repository.", - positionals: [ + name: "basic", + intentHint: "Make checkout timing and mode explicit before creating a managed code checkout.", + generatedDescriptionHint: "Create a basic workstream using a checkout mode.", + options: [ { - generatedDescriptionHint: "HTTPS Git endpoint to clone.", - domain: { semanticName: "SOURCE_HTTPS_URL", validation: "silverwoodFn:httpsGitUrl" }, + longName: "checkout-extent", + generatedDescriptionHint: "Provision now or defer checkout until later.", + domain: { semanticName: "CHECKOUT_EXTENT", extensionalDef: ["full", "skip"] }, + required: true, }, ], + subcommands: { + choices: [ + { + name: "jj-colocated", + generatedDescriptionHint: "Clone an HTTPS Git endpoint with a colocated jj repository.", + positionals: [ + { + generatedDescriptionHint: "HTTPS Git endpoint to clone.", + domain: { semanticName: "SOURCE_HTTPS_URL", validation: "silverwoodFn:httpsGitUrl" }, + }, + ], + }, + { + name: "jj-colocated-direnv-unsafe", + generatedDescriptionHint: "Clone with jj, then preapprove the checkout environment with direnv.", + positionals: [ + { + generatedDescriptionHint: "HTTPS Git endpoint to clone.", + domain: { semanticName: "SOURCE_HTTPS_URL", validation: "silverwoodFn:httpsGitUrl" }, + }, + ], + }, + { + name: "apfs-cow", + generatedDescriptionHint: "Copy a local directory using APFS copy-on-write.", + positionals: [ + { + generatedDescriptionHint: "Absolute source directory to copy.", + domain: { semanticName: "ABSOLUTE_PATH", validation: "silverwoodFn:absolutePath" }, + }, + ], + }, + { + name: "apfs-cow-direnv-unsafe", + generatedDescriptionHint: "Copy a local directory using APFS, then preapprove its environment.", + positionals: [ + { + generatedDescriptionHint: "Absolute source directory to copy.", + domain: { semanticName: "ABSOLUTE_PATH", validation: "silverwoodFn:absolutePath" }, + }, + ], + }, + ], + }, }, { - name: "jj-colocated-direnv-unsafe", - generatedDescriptionHint: "Clone with jj, then preapprove the checkout environment with direnv.", + name: "local-unmanaged-existing-path", + generatedDescriptionHint: "Adopt an existing directory without managing it.", positionals: [ { - generatedDescriptionHint: "HTTPS Git endpoint to clone.", - domain: { semanticName: "SOURCE_HTTPS_URL", validation: "silverwoodFn:httpsGitUrl" }, + generatedDescriptionHint: "Absolute path of the existing directory.", + domain: { semanticName: "ABSOLUTE_PATH", validation: "silverwoodFn:absolutePath" }, }, ], }, + { name: "local-tmp", generatedDescriptionHint: "Create a fresh ephemeral directory under /tmp." }, + { name: "local-blank", generatedDescriptionHint: "Create a fresh empty directory inside the forest." }, + ], + }, + }, + { + name: "ls", + generatedDescriptionHint: "List workstreams.", + options: [ + { longName: "all", generatedDescriptionHint: "Include archived and deleted workstreams." }, + ], + }, + { + name: "workstream", + intentHint: "Group actions that affect a specific workstream under its stable id.", + generatedDescriptionHint: "Operate on one workstream by id.", + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + ], + subcommands: { + choices: [ + { name: "show", generatedDescriptionHint: "Show the workstream." }, + { name: "archive", generatedDescriptionHint: "Archive the workstream." }, { - name: "apfs-cow", - generatedDescriptionHint: "Copy a local directory using APFS copy-on-write.", - positionals: [ - { - generatedDescriptionHint: "Absolute source directory to copy.", - domain: { semanticName: "ABSOLUTE_PATH", validation: "silverwoodFn:absolutePath" }, - }, + name: "remove", + generatedDescriptionHint: "Delete a managed workstream and its directory when safe.", + options: [ + { longName: "force", generatedDescriptionHint: "Override the managed directory safety check." }, ], }, { - name: "apfs-cow-direnv-unsafe", - generatedDescriptionHint: "Copy a local directory using APFS, then preapprove its environment.", + name: "rename", + generatedDescriptionHint: "Rename the workstream.", positionals: [ - { - generatedDescriptionHint: "Absolute source directory to copy.", - domain: { semanticName: "ABSOLUTE_PATH", validation: "silverwoodFn:absolutePath" }, - }, + { generatedDescriptionHint: "The new display name.", domain: { semanticName: "NAME" } }, ], }, - ], - }, - { - name: "local-unmanaged-existing-path", - generatedDescriptionHint: "Adopt an existing directory without managing it.", - positionals: [ { - generatedDescriptionHint: "Absolute path of the existing directory.", - domain: { semanticName: "ABSOLUTE_PATH", validation: "silverwoodFn:absolutePath" }, + name: "basic", + generatedDescriptionHint: "Operations specific to a basic workstream.", + subcommands: { + choices: [ + { name: "checkout", generatedDescriptionHint: "Provision a deferred basic checkout." }, + ], + }, }, ], }, - { name: "local-tmp", generatedDescriptionHint: "Create a fresh ephemeral directory under /tmp." }, - { name: "local-blank", generatedDescriptionHint: "Create a fresh empty directory inside the forest." }, - ], - }, - { - name: "ls", - generatedDescriptionHint: "List workstreams.", - options: [ - { longName: "all", generatedDescriptionHint: "Include archived and deleted workstreams." }, - ], - }, - { - name: "workstream", - intentHint: "Group actions that affect a specific workstream under its stable id.", - generatedDescriptionHint: "Operate on one workstream by id.", - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - ], - subcommands: [ - { name: "show", generatedDescriptionHint: "Show the workstream." }, - { name: "archive", generatedDescriptionHint: "Archive the workstream." }, - { - name: "remove", - generatedDescriptionHint: "Delete a managed workstream and its directory when safe.", - options: [ - { longName: "force", generatedDescriptionHint: "Override the managed directory safety check." }, - ], - }, - { - name: "rename", - generatedDescriptionHint: "Rename the workstream.", - positionals: [ - { generatedDescriptionHint: "The new display name.", domain: { semanticName: "NAME" } }, - ], - }, - { - name: "basic", - generatedDescriptionHint: "Operations specific to a basic workstream.", - subcommands: [ - { name: "checkout", generatedDescriptionHint: "Provision a deferred basic checkout." }, - ], - }, - ], - }, - { - name: "kv", - intentHint: "Allow frontends to persist their own per-workstream state without changing core schema.", - generatedDescriptionHint: "Read and write frontend-owned namespaced key-value state.", - subcommands: [ - { - name: "set", - generatedDescriptionHint: "Set a JSON string value.", - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - { generatedDescriptionHint: "Namespace to write.", domain: { semanticName: "NAMESPACE" } }, - { generatedDescriptionHint: "Key within the namespace.", domain: { semanticName: "KEY" } }, - { generatedDescriptionHint: "Opaque JSON string value.", domain: { semanticName: "VALUE" } }, - ], - }, - { - name: "get", - generatedDescriptionHint: "Get one value.", - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - { generatedDescriptionHint: "Namespace containing the key.", domain: { semanticName: "NAMESPACE" } }, - { generatedDescriptionHint: "Key within the namespace.", domain: { semanticName: "KEY" } }, - ], - }, - { - name: "ls", - generatedDescriptionHint: "List every entry in a namespace.", - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - { generatedDescriptionHint: "Namespace to list.", domain: { semanticName: "NAMESPACE" } }, - ], - }, - { - name: "unset", - generatedDescriptionHint: "Remove one value.", - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - { generatedDescriptionHint: "Namespace containing the key.", domain: { semanticName: "NAMESPACE" } }, - { generatedDescriptionHint: "Key to remove.", domain: { semanticName: "KEY" } }, - ], - }, - ], - }, - { - name: "session", - intentHint: "Treat agent and shell sessions as durable records associated with a workstream.", - generatedDescriptionHint: "Manage sessions attached to workstreams.", - subcommands: [ - { - name: "create", - generatedDescriptionHint: "Create a session of a chosen kind.", - subcommands: [ + }, + { + name: "kv", + intentHint: "Allow frontends to persist their own per-workstream state without changing core schema.", + generatedDescriptionHint: "Read and write frontend-owned namespaced key-value state.", + subcommands: { + choices: [ { - name: "claude-code", - generatedDescriptionHint: "Record a Claude Code session.", - options: [ - { longName: "name", generatedDescriptionHint: "Display name; defaults to the session id.", domain: { semanticName: "NAME" } }, + name: "set", + generatedDescriptionHint: "Set a JSON string value.", + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + { generatedDescriptionHint: "Namespace to write.", domain: { semanticName: "NAMESPACE" } }, + { generatedDescriptionHint: "Key within the namespace.", domain: { semanticName: "KEY" } }, + { generatedDescriptionHint: "Opaque JSON string value.", domain: { semanticName: "VALUE" } }, ], + }, + { + name: "get", + generatedDescriptionHint: "Get one value.", positionals: [ { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + { generatedDescriptionHint: "Namespace containing the key.", domain: { semanticName: "NAMESPACE" } }, + { generatedDescriptionHint: "Key within the namespace.", domain: { semanticName: "KEY" } }, ], }, { - name: "codex", - generatedDescriptionHint: "Start Codex and register its generated session id.", - options: [ - { longName: "name", generatedDescriptionHint: "Display name; defaults to the generated id.", domain: { semanticName: "NAME" } }, + name: "ls", + generatedDescriptionHint: "List every entry in a namespace.", + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + { generatedDescriptionHint: "Namespace to list.", domain: { semanticName: "NAMESPACE" } }, + ], + }, + { + name: "unset", + generatedDescriptionHint: "Remove one value.", + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + { generatedDescriptionHint: "Namespace containing the key.", domain: { semanticName: "NAMESPACE" } }, + { generatedDescriptionHint: "Key to remove.", domain: { semanticName: "KEY" } }, ], + }, + ], + }, + }, + { + name: "session", + intentHint: "Treat agent and shell sessions as durable records associated with a workstream.", + generatedDescriptionHint: "Manage sessions attached to workstreams.", + subcommands: { + choices: [ + { + name: "create", + generatedDescriptionHint: "Create a session of a chosen kind.", + subcommands: { + choices: [ + { + name: "claude-code", + generatedDescriptionHint: "Record a Claude Code session.", + options: [ + { longName: "name", generatedDescriptionHint: "Display name; defaults to the session id.", domain: { semanticName: "NAME" } }, + ], + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + ], + }, + { + name: "codex", + generatedDescriptionHint: "Start Codex and register its generated session id.", + options: [ + { longName: "name", generatedDescriptionHint: "Display name; defaults to the generated id.", domain: { semanticName: "NAME" } }, + ], + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + ], + }, + { + name: "plain-shell", + generatedDescriptionHint: "Record a plain login shell session.", + options: [ + { longName: "name", generatedDescriptionHint: "Display name; defaults to the session id.", domain: { semanticName: "NAME" } }, + ], + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + ], + }, + { + name: "claude-code-noninteractiveshell", + generatedDescriptionHint: "Record a Claude Code session run without an interactive shell.", + options: [ + { longName: "name", generatedDescriptionHint: "Display name; defaults to the session id.", domain: { semanticName: "NAME" } }, + { + longName: "run-direnv-exec", + generatedDescriptionHint: "Whether to run claude under direnv exec; supply true or false.", + domain: { semanticName: "RUN_DIRENV_EXEC", extensionalDef: ["true", "false"] }, + required: true, + }, + ], + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + ], + }, + { + name: "disk-space", + generatedDescriptionHint: "Record a disk-space monitor session.", + options: [ + { longName: "name", generatedDescriptionHint: "Display name; defaults to the session id.", domain: { semanticName: "NAME" } }, + ], + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + ], + }, + ], + }, + }, + { + name: "ls", + generatedDescriptionHint: "List sessions on a workstream.", positionals: [ { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, ], }, { - name: "plain-shell", - generatedDescriptionHint: "Record a plain login shell session.", - options: [ - { longName: "name", generatedDescriptionHint: "Display name; defaults to the session id.", domain: { semanticName: "NAME" } }, + name: "rename", + generatedDescriptionHint: "Rename one session.", + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + { generatedDescriptionHint: "The new display name.", domain: { semanticName: "NAME" } }, ], + }, + { + name: "rm", + generatedDescriptionHint: "Remove one session.", positionals: [ { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, ], }, { - name: "claude-code-noninteractiveshell", - generatedDescriptionHint: "Record a Claude Code session run without an interactive shell.", + name: "lock", + generatedDescriptionHint: "Acquire a session advisory lock.", options: [ - { longName: "name", generatedDescriptionHint: "Display name; defaults to the session id.", domain: { semanticName: "NAME" } }, { - longName: "run-direnv-exec", - generatedDescriptionHint: "Whether to run claude under direnv exec; supply true or false.", - domain: { semanticName: "RUN_DIRENV_EXEC", extensionalDef: ["true", "false"] }, + longName: "holder", + generatedDescriptionHint: "Opaque token identifying the lock holder.", + domain: { semanticName: "HOLDER" }, required: true, }, + { longName: "force", generatedDescriptionHint: "Steal an existing lock." }, ], positionals: [ { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, @@ -241,144 +311,52 @@ ], }, { - name: "disk-space", - generatedDescriptionHint: "Record a disk-space monitor session.", + name: "unlock", + generatedDescriptionHint: "Release a session advisory lock.", options: [ - { longName: "name", generatedDescriptionHint: "Display name; defaults to the session id.", domain: { semanticName: "NAME" } }, + { longName: "holder", generatedDescriptionHint: "Only release a lock held by this token.", domain: { semanticName: "HOLDER" } }, + { longName: "force", generatedDescriptionHint: "Release regardless of current holder." }, ], positionals: [ { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, ], }, - ], - }, - { - name: "ls", - generatedDescriptionHint: "List sessions on a workstream.", - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - ], - }, - { - name: "rename", - generatedDescriptionHint: "Rename one session.", - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, - { generatedDescriptionHint: "The new display name.", domain: { semanticName: "NAME" } }, - ], - }, - { - name: "rm", - generatedDescriptionHint: "Remove one session.", - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, - ], - }, - { - name: "lock", - generatedDescriptionHint: "Acquire a session advisory lock.", - options: [ - { - longName: "holder", - generatedDescriptionHint: "Opaque token identifying the lock holder.", - domain: { semanticName: "HOLDER" }, - required: true, - }, - { longName: "force", generatedDescriptionHint: "Steal an existing lock." }, - ], - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, - ], - }, - { - name: "unlock", - generatedDescriptionHint: "Release a session advisory lock.", - options: [ - { longName: "holder", generatedDescriptionHint: "Only release a lock held by this token.", domain: { semanticName: "HOLDER" } }, - { longName: "force", generatedDescriptionHint: "Release regardless of current holder." }, - ], - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, - ], - }, - { - name: "doctor", - generatedDescriptionHint: "Report the health of a recorded session.", - positionals: [ - { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, - { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, - ], - }, - ], - }, - { - name: "upgrade-forest", - generatedDescriptionHint: "Upgrade every document to the latest schema version.", - options: [ - { longName: "dry-run", generatedDescriptionHint: "Report what would change without writing." }, - ], - }, - { name: "new-schema", generatedDescriptionHint: "Describe the new subcommand tree for frontends." }, - { name: "session-schema", generatedDescriptionHint: "Describe session creation kinds for frontends." }, - { - name: "spawn", - intentHint: "Launch a recorded or direct session in the intended working directory; --json reveals the plan.", - generatedDescriptionHint: "Run a session, or inspect its shell plan with --json.", - subcommands: [ - { - name: "from-id", - generatedDescriptionHint: "Run a recorded session in its workstream checkout.", - options: [ - { - longName: "workstream", - generatedDescriptionHint: "Workstream containing the session.", - domain: { semanticName: "WORKSTREAM" }, - required: true, - }, - ], - positionals: [ - { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, - ], - }, - { - name: "plain-shell", - generatedDescriptionHint: "Run a plain interactive login shell.", - options: [ { - longName: "override-working-directory", - generatedDescriptionHint: "Working directory to run in (default: this process's own cwd).", - domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, - }, - ], - }, - { - name: "disk-space", - generatedDescriptionHint: "Run a disk-space monitor.", - options: [ - { - longName: "override-working-directory", - generatedDescriptionHint: "Working directory to run in (default: this process's own cwd).", - domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, + name: "doctor", + generatedDescriptionHint: "Report the health of a recorded session.", + positionals: [ + { generatedDescriptionHint: "The workstream id.", domain: { semanticName: "ID", validation: "silverwoodFn:workstreamId" } }, + { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + ], }, ], }, - { - name: "claude-code", - generatedDescriptionHint: "Run a Claude Code session.", - subcommands: [ + }, + { + name: "upgrade-forest", + generatedDescriptionHint: "Upgrade every document to the latest schema version.", + options: [ + { longName: "dry-run", generatedDescriptionHint: "Report what would change without writing." }, + ], + }, + { name: "new-schema", generatedDescriptionHint: "Describe the new subcommand tree for frontends." }, + { name: "session-schema", generatedDescriptionHint: "Describe session creation kinds for frontends." }, + { + name: "spawn", + intentHint: "Launch a recorded or direct session in the intended working directory; --json reveals the plan.", + generatedDescriptionHint: "Run a session, or inspect its shell plan with --json.", + subcommands: { + choices: [ { - name: "first-run", - generatedDescriptionHint: "First launch of this Claude session id.", + name: "from-id", + generatedDescriptionHint: "Run a recorded session in its workstream checkout.", options: [ { - longName: "override-working-directory", - generatedDescriptionHint: "Working directory to run in (default: this process's own cwd).", - domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, + longName: "workstream", + generatedDescriptionHint: "Workstream containing the session.", + domain: { semanticName: "WORKSTREAM" }, + required: true, }, ], positionals: [ @@ -386,8 +364,8 @@ ], }, { - name: "resume", - generatedDescriptionHint: "Resume an existing Claude conversation.", + name: "plain-shell", + generatedDescriptionHint: "Run a plain interactive login shell.", options: [ { longName: "override-working-directory", @@ -395,27 +373,10 @@ domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, }, ], - positionals: [ - { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, - ], }, - ], - }, - { - name: "claude-code-noninteractiveshell", - generatedDescriptionHint: "Run Claude Code without an interactive shell.", - options: [ { - longName: "run-direnv-exec", - generatedDescriptionHint: "Whether to run claude under direnv exec; supply true or false.", - domain: { semanticName: "RUN_DIRENV_EXEC", extensionalDef: ["true", "false"] }, - required: true, - }, - ], - subcommands: [ - { - name: "first-run", - generatedDescriptionHint: "First launch of this Claude session id.", + name: "disk-space", + generatedDescriptionHint: "Run a disk-space monitor.", options: [ { longName: "override-working-directory", @@ -423,28 +384,91 @@ domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, }, ], - positionals: [ - { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, - ], }, { - name: "resume", - generatedDescriptionHint: "Resume an existing Claude conversation.", + name: "claude-code", + generatedDescriptionHint: "Run a Claude Code session.", + subcommands: { + choices: [ + { + name: "first-run", + generatedDescriptionHint: "First launch of this Claude session id.", + options: [ + { + longName: "override-working-directory", + generatedDescriptionHint: "Working directory to run in (default: this process's own cwd).", + domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, + }, + ], + positionals: [ + { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + ], + }, + { + name: "resume", + generatedDescriptionHint: "Resume an existing Claude conversation.", + options: [ + { + longName: "override-working-directory", + generatedDescriptionHint: "Working directory to run in (default: this process's own cwd).", + domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, + }, + ], + positionals: [ + { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + ], + }, + ], + }, + }, + { + name: "claude-code-noninteractiveshell", + generatedDescriptionHint: "Run Claude Code without an interactive shell.", options: [ { - longName: "override-working-directory", - generatedDescriptionHint: "Working directory to run in (default: this process's own cwd).", - domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, + longName: "run-direnv-exec", + generatedDescriptionHint: "Whether to run claude under direnv exec; supply true or false.", + domain: { semanticName: "RUN_DIRENV_EXEC", extensionalDef: ["true", "false"] }, + required: true, }, ], - positionals: [ - { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, - ], + subcommands: { + choices: [ + { + name: "first-run", + generatedDescriptionHint: "First launch of this Claude session id.", + options: [ + { + longName: "override-working-directory", + generatedDescriptionHint: "Working directory to run in (default: this process's own cwd).", + domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, + }, + ], + positionals: [ + { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + ], + }, + { + name: "resume", + generatedDescriptionHint: "Resume an existing Claude conversation.", + options: [ + { + longName: "override-working-directory", + generatedDescriptionHint: "Working directory to run in (default: this process's own cwd).", + domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, + }, + ], + positionals: [ + { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, + ], + }, + ], + }, }, ], }, - ], - }, - { name: "__register-codex-session", generatedDescriptionHint: "Internal Codex SessionStart callback.", hidden: true }, - ], + }, + { name: "__register-codex-session", generatedDescriptionHint: "Internal Codex SessionStart callback.", hidden: true }, + ], + }, } diff --git a/experimental/silverwood/DESIGN.md b/experimental/silverwood/DESIGN.md index 7ad1e63..8bc413d 100644 --- a/experimental/silverwood/DESIGN.md +++ b/experimental/silverwood/DESIGN.md @@ -262,8 +262,10 @@ emits machine-readable `--json`. Any frontend — Rust (link core), or any langu (shell out to the CLI) — drives the same backend. The human-edited CLI syntax contract is [`CLI_SPEC.jsonnet`](./CLI_SPEC.jsonnet). -Its nested `subcommands` are alternatives; each node's options and positionals -are used together. Options are accepted at the node where declared and below it. +Each `subcommands` object has a `choices` list of alternatives and can carry +its own `intentHint` and `generatedDescriptionHint`. Each command's options and +positionals are used together. Options are accepted at the node where declared +and below it. The CLI crate compares this contract with clap in its test suite. `intentHint` explains the intended interface to implementers, while `generatedDescriptionHint` holds initial agent-generated wording; neither prose field is compared with diff --git a/experimental/silverwood/crates/silverwood-cli/src/contract_test.rs b/experimental/silverwood/crates/silverwood-cli/src/contract_test.rs index dacae25..328929c 100644 --- a/experimental/silverwood/crates/silverwood-cli/src/contract_test.rs +++ b/experimental/silverwood/crates/silverwood-cli/src/contract_test.rs @@ -24,11 +24,22 @@ struct CommandSpec { #[serde(default)] positionals: Vec, #[serde(default)] - subcommands: Vec, + subcommands: SubcommandsSpec, #[serde(default)] hidden: bool, } +#[derive(Default, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct SubcommandsSpec { + #[serde(default)] + intent_hint: Option, + #[serde(default)] + generated_description_hint: Option, + #[serde(default)] + choices: Vec, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct OptionSpec { @@ -110,9 +121,14 @@ fn compare_command(actual: &clap::Command, spec: &CommandSpec, path: &str) { spec.generated_description_hint.as_deref(), path, ); + check_hints( + spec.subcommands.intent_hint.as_deref(), + spec.subcommands.generated_description_hint.as_deref(), + &format!("{path} subcommands"), + ); assert_eq!( actual.is_subcommand_required_set(), - !spec.subcommands.is_empty(), + !spec.subcommands.choices.is_empty(), "subcommand requirement at {path}" ); @@ -146,7 +162,7 @@ fn compare_command(actual: &clap::Command, spec: &CommandSpec, path: &str) { // clap disallows a required global argument. Those options are checked // after parsing, and their missing-value behavior is covered by CLI tests. // A new case must be explicitly reviewed and added here with a runtime test. - if expected.required && !spec.subcommands.is_empty() { + if expected.required && !spec.subcommands.choices.is_empty() { assert!( matches!( (path, expected.long_name.as_str()), @@ -159,13 +175,13 @@ fn compare_command(actual: &clap::Command, spec: &CommandSpec, path: &str) { "new inherited required option needs a runtime check: {label}" ); } - let clap_required = expected.required && spec.subcommands.is_empty(); + let clap_required = expected.required && spec.subcommands.choices.is_empty(); assert_eq!( actual.is_required_set(), clap_required, "required at {label}" ); - if !spec.subcommands.is_empty() { + if !spec.subcommands.choices.is_empty() { assert!(actual.is_global_set(), "{label} must apply to descendants"); } check_hints( @@ -209,11 +225,11 @@ fn compare_command(actual: &clap::Command, spec: &CommandSpec, path: &str) { let children: Vec<_> = actual.get_subcommands().collect(); assert_eq!( children.len(), - spec.subcommands.len(), + spec.subcommands.choices.len(), "subcommand count at {path}; actual: {:?}", children.iter().map(|c| c.get_name()).collect::>() ); - for (child, expected) in children.into_iter().zip(&spec.subcommands) { + for (child, expected) in children.into_iter().zip(&spec.subcommands.choices) { compare_command(child, expected, &format!("{path} {}", expected.name)); } } diff --git a/experimental/silverwood/crates/silverwood-cli/src/main.rs b/experimental/silverwood/crates/silverwood-cli/src/main.rs index 23b0e02..1dd8a86 100644 --- a/experimental/silverwood/crates/silverwood-cli/src/main.rs +++ b/experimental/silverwood/crates/silverwood-cli/src/main.rs @@ -58,6 +58,8 @@ enum Command { /// Human-friendly name (accepted at any `new` subcommand level). #[arg(long, global = true)] name: Option, + /// Intent of these choices: choose the kind of workstream to create. + /// Generated description hint: available workstream kinds. #[command(subcommand)] variant: NewVariant, }, -- 2.54.0