From 6fc8809d220411c53d7ff86a6c8dcad1e2505fa0 Mon Sep 17 00:00:00 2001 From: Yuto Nishida Date: Sun, 4 Oct 2026 16:43:48 -0700 Subject: [PATCH] silverwood: simplify Jsonnet CLI spec and default omitted fields in consumer --- experimental/silverwood/CLI_SPEC.jsonnet | 656 ++++++++++++------ experimental/silverwood/DESIGN.md | 7 +- .../silverwood-cli/src/contract_test.rs | 69 +- 3 files changed, 499 insertions(+), 233 deletions(-) diff --git a/experimental/silverwood/CLI_SPEC.jsonnet b/experimental/silverwood/CLI_SPEC.jsonnet index cea7533..7e0aee4 100644 --- a/experimental/silverwood/CLI_SPEC.jsonnet +++ b/experimental/silverwood/CLI_SPEC.jsonnet @@ -1,212 +1,448 @@ -// Human-edited algebraic contract for the public `silverwood` CLI. -// Each command's options and positionals form a product; its subcommands form a sum. -// An option applies at its declaring command and every descendant. -// generatedDescriptionHint is an initial agent-generated description; edit intentHint -// to explain the interface decision to future implementers. -// `validation` identifies intended behavior, but the initial conformance check does -// not inspect imperative Rust validators. +// Human-edited algebraic contract for the silverwood CLI. +// A command's options and positionals form a product; subcommands form a sum. +// Options apply at the declaring command and every descendant. +// Missing child lists 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. -local domain(semanticName=null, validation=null, extensionalDef=null) = - (if semanticName == null then {} else { semanticName: semanticName }) + - (if validation == null then {} else { validation: validation }) + - (if extensionalDef == null then {} else { extensionalDef: extensionalDef }); - -local command(name, description, options=[], positionals=[], subcommands=[], intent=null, hidden=false) = { - name: name, - intentHint: if intent == null then description else intent, - generatedDescriptionHint: description, - options: options, - positionals: positionals, - subcommands: subcommands, - hidden: hidden, -}; - -local option(longName, description, valueDomain=null, required=false, intent=null) = { - longName: longName, - arity: if valueDomain == null then 0 else 1, - intentHint: if intent == null then description else intent, - generatedDescriptionHint: description, - required: required, -} + (if valueDomain == null then {} else { domain: valueDomain }); - -local positional(description, valueDomain, intent=null) = { - domain: valueDomain, - required: true, - intentHint: if intent == null then description else intent, - generatedDescriptionHint: description, -}; - -local id = function(what='workstream') - positional('The ' + what + ' id.', domain('ID', 'silverwoodFn:workstreamId')); -local sessionId = positional('The session id.', domain('SESSION_ID')); -local name = positional('The new display name.', domain('NAME')); -local workingDir = option( - 'override-working-directory', - 'Working directory to run in (default: this process\'s own cwd).', - domain('PATH', 'rustType:std::path::Path') -); -local runDirenv = option( - 'run-direnv-exec', - 'Whether to run claude under direnv exec; supply true or false.', - domain('RUN_DIRENV_EXEC', extensionalDef=['true', 'false']), - required=true -); -local firstRun = command( - 'first-run', 'First launch of this Claude session id.', - positionals=[sessionId], options=[workingDir] -); -local resume = command( - 'resume', 'Resume an existing Claude conversation.', - positionals=[sessionId], options=[workingDir] -); - -command( - 'silverwood', - 'Frontend-agnostic backend for code and agent workstreams.', - intent='Expose the same persistent workstream and session state to humans and shell-driven frontends.', - options=[ - option('forest', 'Path to the forest; overrides the environment and home default.', - domain('DIR', 'rustType:std::path::Path')), - option('json', 'Emit machine-readable JSON instead of human-readable text.'), +{ + name: "silverwood", + intentHint: "Expose the same persistent workstream and session state to humans and shell-driven frontends.", + generatedDescriptionHint: "Frontend-agnostic backend for code and agent workstreams.", + options: [ + { + longName: "forest", + generatedDescriptionHint: "Path to the forest; overrides the environment and home default.", + domain: { semanticName: "DIR", validation: "rustType:std::path::Path" }, + }, + { 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: [ + { + 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: [ + { + 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: "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: "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: "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: "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: "claude-code", + generatedDescriptionHint: "Run a Claude Code session.", + subcommands: [ + { + 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: "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.", + 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 }, ], - subcommands=[ - command('info', 'Open the forest and print its identity.'), - command( - 'new', 'Create a workstream.', - intent='Make the user choose a workstream kind and supply a human name.', - options=[option('name', 'Human-friendly workstream name.', domain('NAME'))], - subcommands=[ - command( - 'basic', 'Create a basic workstream using a checkout mode.', - intent='Make checkout timing and mode explicit before creating a managed code checkout.', - options=[option( - 'checkout-extent', 'Provision now or defer checkout until later.', - domain('CHECKOUT_EXTENT', extensionalDef=['full', 'skip']), required=true - )], - subcommands=[ - command('jj-colocated', 'Clone an HTTPS Git endpoint with a colocated jj repository.', - positionals=[positional('HTTPS Git endpoint to clone.', - domain('SOURCE_HTTPS_URL', 'silverwoodFn:httpsGitUrl'))]), - command('jj-colocated-direnv-unsafe', - 'Clone with jj, then preapprove the checkout environment with direnv.', - positionals=[positional('HTTPS Git endpoint to clone.', - domain('SOURCE_HTTPS_URL', 'silverwoodFn:httpsGitUrl'))]), - command('apfs-cow', 'Copy a local directory using APFS copy-on-write.', - positionals=[positional('Absolute source directory to copy.', - domain('ABSOLUTE_PATH', 'silverwoodFn:absolutePath'))]), - command('apfs-cow-direnv-unsafe', - 'Copy a local directory using APFS, then preapprove its environment.', - positionals=[positional('Absolute source directory to copy.', - domain('ABSOLUTE_PATH', 'silverwoodFn:absolutePath'))]), - ] - ), - command('local-unmanaged-existing-path', 'Adopt an existing directory without managing it.', - positionals=[positional('Absolute path of the existing directory.', - domain('ABSOLUTE_PATH', 'silverwoodFn:absolutePath'))]), - command('local-tmp', 'Create a fresh ephemeral directory under /tmp.'), - command('local-blank', 'Create a fresh empty directory inside the forest.'), - ] - ), - command('ls', 'List workstreams.', - options=[option('all', 'Include archived and deleted workstreams.')]), - command( - 'workstream', 'Operate on one workstream by id.', - intent='Group actions that affect a specific workstream under its stable id.', - positionals=[id()], - subcommands=[ - command('show', 'Show the workstream.'), - command('archive', 'Archive the workstream.'), - command('remove', 'Delete a managed workstream and its directory when safe.', - options=[option('force', 'Override the managed directory safety check.')]), - command('rename', 'Rename the workstream.', positionals=[name]), - command('basic', 'Operations specific to a basic workstream.', - subcommands=[command('checkout', 'Provision a deferred basic checkout.')]), - ] - ), - command( - 'kv', 'Read and write frontend-owned namespaced key-value state.', - intent='Allow frontends to persist their own per-workstream state without changing core schema.', - subcommands=[ - command('set', 'Set a JSON string value.', positionals=[ - id(), - positional('Namespace to write.', domain('NAMESPACE')), - positional('Key within the namespace.', domain('KEY')), - positional('Opaque JSON string value.', domain('VALUE')), - ]), - command('get', 'Get one value.', positionals=[ - id(), - positional('Namespace containing the key.', domain('NAMESPACE')), - positional('Key within the namespace.', domain('KEY')), - ]), - command('ls', 'List every entry in a namespace.', positionals=[ - id(), positional('Namespace to list.', domain('NAMESPACE')), - ]), - command('unset', 'Remove one value.', positionals=[ - id(), - positional('Namespace containing the key.', domain('NAMESPACE')), - positional('Key to remove.', domain('KEY')), - ]), - ] - ), - command( - 'session', 'Manage sessions attached to workstreams.', - intent='Treat agent and shell sessions as durable records associated with a workstream.', - subcommands=[ - command( - 'create', 'Create a session of a chosen kind.', - subcommands=[ - command('claude-code', 'Record a Claude Code session.', - options=[option('name', 'Display name; defaults to the session id.', domain('NAME'))], - positionals=[id(), sessionId]), - command('codex', 'Start Codex and register its generated session id.', - options=[option('name', 'Display name; defaults to the generated id.', domain('NAME'))], - positionals=[id()]), - command('plain-shell', 'Record a plain login shell session.', - options=[option('name', 'Display name; defaults to the session id.', domain('NAME'))], - positionals=[id(), sessionId]), - command('claude-code-noninteractiveshell', - 'Record a Claude Code session run without an interactive shell.', - options=[option('name', 'Display name; defaults to the session id.', domain('NAME')), - runDirenv], positionals=[id(), sessionId]), - command('disk-space', 'Record a disk-space monitor session.', - options=[option('name', 'Display name; defaults to the session id.', domain('NAME'))], - positionals=[id(), sessionId]), - ] - ), - command('ls', 'List sessions on a workstream.', positionals=[id()]), - command('rename', 'Rename one session.', positionals=[id(), sessionId, name]), - command('rm', 'Remove one session.', positionals=[id(), sessionId]), - command('lock', 'Acquire a session advisory lock.', - options=[option('holder', 'Opaque token identifying the lock holder.', domain('HOLDER'), required=true), - option('force', 'Steal an existing lock.')], - positionals=[id(), sessionId]), - command('unlock', 'Release a session advisory lock.', - options=[option('holder', 'Only release a lock held by this token.', domain('HOLDER')), - option('force', 'Release regardless of current holder.')], - positionals=[id(), sessionId]), - command('doctor', 'Report the health of a recorded session.', - positionals=[id(), sessionId]), - ] - ), - command('upgrade-forest', 'Upgrade every document to the latest schema version.', - options=[option('dry-run', 'Report what would change without writing.')]), - command('new-schema', 'Describe the new subcommand tree for frontends.'), - command('session-schema', 'Describe session creation kinds for frontends.'), - command( - 'spawn', 'Run a session, or inspect its shell plan with --json.', - intent='Launch a recorded or direct session in the intended working directory; --json reveals the plan.', - subcommands=[ - command('from-id', 'Run a recorded session in its workstream checkout.', - options=[option('workstream', 'Workstream containing the session.', domain('WORKSTREAM'), required=true)], - positionals=[sessionId]), - command('plain-shell', 'Run a plain interactive login shell.', options=[workingDir]), - command('disk-space', 'Run a disk-space monitor.', options=[workingDir]), - command('claude-code', 'Run a Claude Code session.', subcommands=[firstRun, resume]), - command('claude-code-noninteractiveshell', - 'Run Claude Code without an interactive shell.', - options=[runDirenv], subcommands=[firstRun, resume]), - ] - ), - command('__register-codex-session', 'Internal Codex SessionStart callback.', hidden=true), - ] -) +} diff --git a/experimental/silverwood/DESIGN.md b/experimental/silverwood/DESIGN.md index 7fb3139..7ad1e63 100644 --- a/experimental/silverwood/DESIGN.md +++ b/experimental/silverwood/DESIGN.md @@ -267,8 +267,11 @@ 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 -clap. Validator URIs express intended value checks but are not yet statically -matched to the Rust handlers. +clap. The Jsonnet is a plain nested object: omit empty child lists, optional +hints, false flags, and inferred arity. The test consumer supplies those +defaults, and treats positionals as required unless stated otherwise. Validator +URIs express intended value checks but are not yet statically matched to the +Rust handlers. Provisional core surface (illustrative, not final): diff --git a/experimental/silverwood/crates/silverwood-cli/src/contract_test.rs b/experimental/silverwood/crates/silverwood-cli/src/contract_test.rs index be893da..dacae25 100644 --- a/experimental/silverwood/crates/silverwood-cli/src/contract_test.rs +++ b/experimental/silverwood/crates/silverwood-cli/src/contract_test.rs @@ -15,8 +15,10 @@ use super::Cli; #[serde(rename_all = "camelCase", deny_unknown_fields)] struct CommandSpec { name: String, - intent_hint: String, - generated_description_hint: String, + #[serde(default)] + intent_hint: Option, + #[serde(default)] + generated_description_hint: Option, #[serde(default)] options: Vec, #[serde(default)] @@ -31,25 +33,42 @@ struct CommandSpec { #[serde(rename_all = "camelCase", deny_unknown_fields)] struct OptionSpec { long_name: String, - arity: usize, + #[serde(default)] + arity: Option, #[serde(default)] domain: Option, #[serde(default)] required: bool, - intent_hint: String, - generated_description_hint: String, + #[serde(default)] + intent_hint: Option, + #[serde(default)] + generated_description_hint: Option, +} + +impl OptionSpec { + fn arity(&self) -> usize { + self.arity.unwrap_or(usize::from(self.domain.is_some())) + } } #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct PositionalSpec { + #[serde(default)] domain: DomainSpec, + #[serde(default = "default_true")] required: bool, - intent_hint: String, - generated_description_hint: String, + #[serde(default)] + intent_hint: Option, + #[serde(default)] + generated_description_hint: Option, } -#[derive(Deserialize)] +fn default_true() -> bool { + true +} + +#[derive(Default, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct DomainSpec { #[serde(default)] @@ -86,7 +105,11 @@ fn compare_command(actual: &clap::Command, spec: &CommandSpec, path: &str) { 0, "unmodeled alias at {path}" ); - check_hints(&spec.intent_hint, &spec.generated_description_hint, path); + check_hints( + spec.intent_hint.as_deref(), + spec.generated_description_hint.as_deref(), + path, + ); assert_eq!( actual.is_subcommand_required_set(), !spec.subcommands.is_empty(), @@ -119,7 +142,7 @@ fn compare_command(actual: &clap::Command, spec: &CommandSpec, path: &str) { "unmodeled alias: {label}" ); assert!(!actual.is_hide_set(), "unmodeled hidden option: {label}"); - assert_eq!(arity(actual), expected.arity, "arity at {label}"); + assert_eq!(arity(actual), expected.arity(), "arity at {label}"); // 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. @@ -146,11 +169,11 @@ fn compare_command(actual: &clap::Command, spec: &CommandSpec, path: &str) { assert!(actual.is_global_set(), "{label} must apply to descendants"); } check_hints( - &expected.intent_hint, - &expected.generated_description_hint, + expected.intent_hint.as_deref(), + expected.generated_description_hint.as_deref(), &label, ); - match (&expected.domain, expected.arity) { + match (&expected.domain, expected.arity()) { (None, 0) => {} (Some(domain), 1) => compare_domain(actual, domain, &label), _ => panic!("a one-value option needs a domain and a flag has none: {label}"), @@ -176,8 +199,8 @@ fn compare_command(actual: &clap::Command, spec: &CommandSpec, path: &str) { "required at {label}" ); check_hints( - &expected.intent_hint, - &expected.generated_description_hint, + expected.intent_hint.as_deref(), + expected.generated_description_hint.as_deref(), &label, ); compare_domain(actual, &expected.domain, &label); @@ -251,10 +274,14 @@ fn compare_domain(arg: &Arg, domain: &DomainSpec, label: &str) { ); } -fn check_hints(intent: &str, generated: &str, label: &str) { - assert!(!intent.trim().is_empty(), "empty intentHint at {label}"); - assert!( - !generated.trim().is_empty(), - "empty generatedDescriptionHint at {label}" - ); +fn check_hints(intent: Option<&str>, generated: Option<&str>, label: &str) { + if let Some(intent) = intent { + assert!(!intent.trim().is_empty(), "empty intentHint at {label}"); + } + if let Some(generated) = generated { + assert!( + !generated.trim().is_empty(), + "empty generatedDescriptionHint at {label}" + ); + } } -- 2.54.0