diff --git a/completions/trsd.bash b/completions/trsd.bash index 8b8cb31..9891d2e 100644 --- a/completions/trsd.bash +++ b/completions/trsd.bash @@ -1,6 +1,6 @@ # Generated from treeseed.command-tree/v1. _trsd_complete() { - local paths="inbox\nsend\ntopics list\ntopics show\ntopics subscribe\ntopics unsubscribe\ncapabilities list\ncapabilities show\nauth login\nauth logout\nauth status\nusers create\nteams list\nteams current\nteams use\nsecrets list\nsecrets status\nsecrets unlock\nsecrets lock\nservices credentials show\nservices credentials put\nservices credentials delete\nservices credentials validate\nplatform verify\nplatform workset\nplatform project create\nplatform topology plan\nplatform topology apply\nplatform topology status\nplatform topology rollback\ndev host activate\ndev host status\ndev host deactivate\ndev host guest image import\ndev session start\ndev session stop\ndev session recover\ndev use\ndev rebuild\ndev restart\ndev status\ndev logs\ndev plan\ndev freeze\ndev verify\nhost initialize\nhost status\nhost doctor\nhost plan\nhost apply\nhost reconcile\nhost events\nhost config show\nhost config plan\nhost config apply\nhost config adopt\nhost postgres transfer prepare\nhost postgres transfer status\nhost topology\nhost connections\nhost provider status\nhost provider credentials list\nhost provider credentials status\nhost provider credentials initialize\nhost provider environment list\nhost provider environment show\nhost provider environment status\nhost provider environment set\nhost provider environment import\nhost provider environment unset\nhost provider environment rotate\nhost provider environment verify\nhost storage status\nhost storage connect\nhost storage reconcile\nhost storage rotate\nhost storage reset\nhost security plan\nhost security initialize\nhost security status\nhost security verify\nhost security rotate\nhost security recovery verify\nhost sandbox status\nhost sandbox doctor\nhost fleet status\nhost update status\nhost update check\nhost update apply\nhost update channel\nhost update pause\nhost update resume\nhost component list\nhost component status\nhost component enable\nhost component disable\nhost aliases list\nhost recovery status\nhost recovery retry\nhost recovery restore\nhost bootstrap status\nhost bootstrap enroll\nhost reset\nhost uninstall\nagents list\nagents show\nagents validate\nagents diff\nagents diagnose\nagents classes list\nagents classes show\nagents bindings list\nagents bindings show\nagents bindings explain\nproviders list\nproviders show\nproviders status\nproviders diagnose\nproviders connect\nproviders disconnect\nproviders registration code status\nproviders registration code reveal\nproviders registration code rotate\nproviders environments list\nproviders environments show\nproviders environments grant\nproviders environments revoke\nproviders requests list\nproviders requests show\nproviders requests approve\nproviders requests reject\nproviders credentials status\nproviders credentials rotate\nproviders credentials revoke\nproviders offers show\nproviders offers validate\nproviders offers plan\nproviders offers apply\nseeds validate\nseeds plan\nseeds apply\nseeds show\nseeds verify\ncapacity status\ncapacity explain\ncapacity usage\ncapacity ledger\ncapacity audit\nplans list\nplans show\nplans explain\nplans diff\nworkdays profiles list\nworkdays profiles show\nworkdays profiles reconcile\nworkdays profiles validate\nworkdays plan\nworkdays start\nworkdays list\nworkdays show\nworkdays watch\nworkdays pause\nworkdays resume\nworkdays stop\nworkdays cancel\nworkdays schedules list\nworkdays schedules show\nworkdays schedules plan\nworkdays schedules start\nworkdays schedules pause\nworkdays schedules resume\nworkdays schedules retire\nassignments list\nassignments show\nassignments explain\nassignments watch\nassignments retry\nassignments cancel\nassignments artifacts\nprojects treedx show\nprojects treedx bind\nprojects treedx status\nprojects treedx diagnose\nprojects treedx capabilities\nprojects treedx workspaces list\nprojects treedx workspaces show\nprojects treedx workspaces abandon\nai status\nai mode show\nai mode set\nai inference models\nai inference jobs\nai inference rollback\nai training libraries\nai training jobs\nai training runs\nai lab status\nai lab agents\nai lab libraries\nai storage show\nai storage connect\nai storage disconnect\nai storage verify\nlibrary show\nlibrary status\nlibrary paths\nlibrary read\nlibrary search\nlibrary query\nlibrary context\nlibrary workspace create\nlibrary workspace show\nlibrary workspace read\nlibrary workspace diff\nlibrary workspace write\nlibrary workspace submit\nlibrary workspace abandon\nlibrary reviews list\nlibrary reviews decide\nlibrary reviews publish\nsave\nstage\nrelease\nstatus\ndiagnose" + local paths="inbox\nsend\ntopics list\ntopics show\ntopics subscribe\ntopics unsubscribe\ncapabilities list\ncapabilities show\nauth login\nauth logout\nauth status\nusers create\nteams list\nteams current\nteams use\nproposals list\nproposals show\nproposals create\nproposals update\nproposals open\nproposals feedback resolve\nproposals voting start\nproposals vote\nproposals evaluate\ndecisions list\ndecisions show\nsecrets list\nsecrets status\nsecrets unlock\nsecrets lock\nservices credentials show\nservices credentials put\nservices credentials delete\nservices credentials validate\nplatform verify\nplatform workset\nplatform project create\nplatform topology plan\nplatform topology apply\nplatform topology status\nplatform topology rollback\ndev host activate\ndev host status\ndev host deactivate\ndev host guest image import\ndev session start\ndev session stop\ndev session recover\ndev use\ndev rebuild\ndev migrate\ndev restart\ndev status\ndev logs\ndev plan\ndev freeze\ndev verify\nhost initialize\nhost status\nhost doctor\nhost plan\nhost apply\nhost reconcile\nhost events\nhost config show\nhost config plan\nhost config apply\nhost config adopt\nhost postgres transfer prepare\nhost postgres transfer status\nhost topology\nhost connections\nhost provider status\nhost provider credentials list\nhost provider credentials status\nhost provider credentials initialize\nhost provider environment list\nhost provider environment show\nhost provider environment status\nhost provider environment set\nhost provider environment import\nhost provider environment unset\nhost provider environment rotate\nhost provider environment verify\nhost storage status\nhost storage connect\nhost storage reconcile\nhost storage rotate\nhost storage reset\nhost security plan\nhost security initialize\nhost security status\nhost security verify\nhost security rotate\nhost security recovery verify\nhost sandbox status\nhost sandbox doctor\nhost fleet status\nhost update status\nhost update check\nhost update apply\nhost update channel\nhost update pause\nhost update resume\nhost component list\nhost component status\nhost component enable\nhost component disable\nhost aliases list\nhost recovery status\nhost recovery retry\nhost recovery restore\nhost bootstrap status\nhost bootstrap enroll\nhost reset\nhost uninstall\nagents list\nagents show\nagents team clone plan\nagents team clone apply\nagents handlers list\nagents handlers show\nagents profiles show\nagents profiles validate\nagents classes list\nagents classes show\nproviders list\nproviders show\nproviders status\nproviders diagnose\nproviders connect\nproviders disconnect\nproviders registration code status\nproviders registration code reveal\nproviders registration code rotate\nproviders environments list\nproviders environments show\nproviders environments grant\nproviders environments revoke\nproviders requests list\nproviders requests show\nproviders requests approve\nproviders requests reject\nproviders credentials status\nproviders credentials rotate\nproviders credentials revoke\nproviders offers show\nproviders offers validate\nproviders offers plan\nproviders offers apply\nseeds validate\nseeds plan\nseeds apply\nseeds show\nseeds verify\ncapacity status\ncapacity explain\ncapacity usage\ncapacity ledger\ncapacity audit\nworkdays profiles list\nworkdays profiles show\nworkdays profiles reconcile\nworkdays profiles validate\nworkdays plan\nworkdays start\nworkdays list\nworkdays show\nworkdays watch\nworkdays stop\nworkdays schedules list\nworkdays schedules show\nworkdays schedules plan\nworkdays schedules start\nworkdays schedules pause\nworkdays schedules resume\nworkdays schedules retire\nassignments list\nassignments show\nassignments explain\nassignments watch\nassignments retry\nassignments cancel\nassignments artifacts\nexecution graph show\nexecution graph watch\nexecution node show\nexecution node explain\nexecution reconcile\nexecution assignments list\nprojects treedx show\nprojects treedx bind\nprojects treedx status\nprojects treedx diagnose\nprojects treedx capabilities\nprojects treedx workspaces list\nprojects treedx workspaces show\nprojects treedx workspaces abandon\nai status\nai mode show\nai mode set\nai inference models\nai inference jobs\nai inference rollback\nai training libraries\nai training jobs\nai training runs\nai lab status\nai lab agents\nai lab libraries\nai storage show\nai storage connect\nai storage disconnect\nai storage verify\nlibrary show\nlibrary status\nlibrary paths\nlibrary read\nlibrary search\nlibrary query\nlibrary context\nlibrary workspace create\nlibrary workspace show\nlibrary workspace read\nlibrary workspace diff\nlibrary workspace write\nlibrary workspace submit\nlibrary workspace abandon\nlibrary reviews list\nlibrary reviews decide\nlibrary reviews publish\nsave\nstage\nrelease\nstatus\ndiagnose" COMPREPLY=( $(compgen -W "$paths" -- "${COMP_WORDS[*]:1}") ) } complete -F _trsd_complete trsd diff --git a/docs/command-reference.md b/docs/command-reference.md index 06d0503..88ebe1a 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -22,7 +22,7 @@ Operation: mutation. Result schema: `treeseed.communication-send-receipt/v4`. Control-plane operation: `communications.send`. - `--server `: Control-plane server profile or URL. -- `--team `: Team id or slug. +- `--team `: One-command team override. - `--json`: Emit the stable JSON envelope. - `--idempotency-key `: Reuse the same request identity when retrying this mutation. - `--plan`: Return the exact proposed outcome without mutation. @@ -46,9 +46,9 @@ Control-plane operation: `communications.topics.list`. - `--server `: Control-plane server profile or URL. - `--team `: Team id or slug. -- `--status `: Status filter. -- `--limit `: Page size. -- `--cursor `: Opaque page cursor. +- `--status `: Topic status. +- `--limit `: Maximum topics. +- `--cursor `: Pagination cursor. - `--json`: Emit the stable JSON envelope. ### trsd topics show @@ -100,9 +100,9 @@ Operation: read. Result schema: `treeseed.capability-page/v1`. Control-plane operation: `capabilities.list`. - `--server `: Control-plane server profile or URL. -- `--status `: Status filter. -- `--limit `: Page size. -- `--cursor `: Opaque page cursor. +- `--status `: Definition status. +- `--limit `: Maximum definitions. +- `--cursor `: Pagination cursor. - `--json`: Emit the stable JSON envelope. - `--family `: Capability family filter. - `--namespace `: Namespace filter. @@ -213,6 +213,173 @@ Execution: `local.teams.use`. - `--json`: Emit the stable JSON envelope. - `--plan`: Return the exact proposed outcome without mutation. +## trsd proposals + +Proposals operations. + +### trsd proposals list + +List the selected resource. + +Operation: read. Result schema: `treeseed.command.list/v1`. +Control-plane operation: `governance.proposals.list`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--status `: Status filter. +- `--limit `: Page size. +- `--cursor `: Opaque page cursor. +- `--json`: Emit the stable JSON envelope. + +### trsd proposals show + +Show the selected resource. + +Operation: read. Result schema: `treeseed.command.show/v1`. +Control-plane operation: `governance.proposals.show`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--json`: Emit the stable JSON envelope. + +### trsd proposals create + +Create the selected resource. + +Operation: mutation. Result schema: `treeseed.command.create/v1`. +Control-plane operation: `governance.proposals.create`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--json`: Emit the stable JSON envelope. +- `--idempotency-key `: Reuse the same request identity when retrying this mutation. +- `--plan`: Return the exact proposed outcome without mutation. + +### trsd proposals update + +Update the selected resource. + +Operation: mutation. Result schema: `treeseed.command.update/v1`. +Control-plane operation: `governance.proposals.update`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--json`: Emit the stable JSON envelope. +- `--if-match `: Exact current resource version, or new when unconfigured. +- `--idempotency-key `: Reuse the same request identity when retrying this mutation. +- `--plan`: Return the exact proposed outcome without mutation. +- `--input `: YAML or JSON proposal update. + +### trsd proposals open + +Open the selected resource. + +Operation: mutation. Result schema: `treeseed.command.open/v1`. +Control-plane operation: `governance.proposals.open`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--json`: Emit the stable JSON envelope. +- `--if-match `: Exact current resource version, or new when unconfigured. +- `--idempotency-key `: Reuse the same request identity when retrying this mutation. +- `--plan`: Return the exact proposed outcome without mutation. + +## trsd proposals feedback + +Feedback operations. + +### trsd proposals feedback resolve + +Resolve the selected resource. + +Operation: mutation. Result schema: `treeseed.command.resolve/v1`. +Control-plane operation: `governance.proposals.feedback.resolve`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--json`: Emit the stable JSON envelope. +- `--if-match `: Exact current resource version, or new when unconfigured. +- `--idempotency-key `: Reuse the same request identity when retrying this mutation. +- `--plan`: Return the exact proposed outcome without mutation. +- `--feedback `: Exact blocking feedback event identity. +- `--input `: YAML or JSON resolution evidence. + +## trsd proposals voting + +Voting operations. + +### trsd proposals voting start + +Start the selected resource. + +Operation: mutation. Result schema: `treeseed.command.start/v1`. +Control-plane operation: `governance.proposals.voting.start`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--json`: Emit the stable JSON envelope. +- `--if-match `: Exact current resource version, or new when unconfigured. +- `--idempotency-key `: Reuse the same request identity when retrying this mutation. +- `--plan`: Return the exact proposed outcome without mutation. + +### trsd proposals vote + +Vote the selected resource. + +Operation: mutation. Result schema: `treeseed.command.vote/v1`. +Control-plane operation: `governance.proposals.vote`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--json`: Emit the stable JSON envelope. +- `--idempotency-key `: Reuse the same request identity when retrying this mutation. +- `--plan`: Return the exact proposed outcome without mutation. +- `--input `: YAML or JSON vote. + +### trsd proposals evaluate + +Evaluate the selected resource. + +Operation: mutation. Result schema: `treeseed.command.evaluate/v1`. +Control-plane operation: `governance.proposals.evaluate`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--json`: Emit the stable JSON envelope. +- `--if-match `: Exact current resource version, or new when unconfigured. +- `--idempotency-key `: Reuse the same request identity when retrying this mutation. +- `--plan`: Return the exact proposed outcome without mutation. +- `--input `: Optional YAML or JSON evaluation decision. + +## trsd decisions + +Decisions operations. + +### trsd decisions list + +List the selected resource. + +Operation: read. Result schema: `treeseed.command.list/v1`. +Control-plane operation: `governance.decisions.list`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--status `: Status filter. +- `--limit `: Page size. +- `--cursor `: Opaque page cursor. +- `--json`: Emit the stable JSON envelope. + +### trsd decisions show + +Show the selected resource. + +Operation: read. Result schema: `treeseed.command.show/v1`. +Control-plane operation: `governance.decisions.show`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--json`: Emit the stable JSON envelope. + ## trsd secrets Secrets operations. @@ -322,7 +489,7 @@ Verify a declarative Platform repository without package checkouts or a control- Operation: read. Result schema: `treeseed.platform-verification/v1`. Execution: `local.platform.verify`. -- `--json`: Emit the stable JSON envelope. +- `--json`: Emit the stable command-result envelope. - `--profile `: Composable profile to verify. ### trsd platform workset @@ -332,7 +499,7 @@ Plan or safely materialize exact primary source checkouts beneath packages/. Operation: mutation. Result schema: `treeseed.platform-workset-result/v1`. Execution: `local.platform.workset`. -- `--json`: Emit the stable JSON envelope. +- `--json`: Emit the stable command-result envelope. - `--plan`: Return the exact proposed outcome without mutation. - `--apply`: Apply the frozen workset plan. - `--yes`: Confirm the planned checkout mutations. @@ -351,8 +518,8 @@ Plan or reconcile a project, repository, template, library binding, and live inv Operation: mutation. Result schema: `treeseed.platform-project-create-result/v1`. Execution: `local.platform.project.create`. -- `--yes`: Confirm authorized automation. -- `--json`: Emit the stable JSON envelope. +- `--yes`: Confirm authority-bearing project creation. +- `--json`: Emit the stable command-result envelope. - `--plan`: Return the exact proposed outcome without mutation. - `--apply`: Apply the accepted creation plan. - `--template `: Published template identity. @@ -379,7 +546,7 @@ Apply an exact agent-authorized hosted-topology plan through the operations runn Operation: mutation. Result schema: `treeseed.platform-operation/v1`. Execution: `local.platform.topology.apply`. -- `--yes`: Confirm authorized automation. +- `--yes`: Confirm the authority-bearing mutation. - `--json`: Emit the stable JSON envelope. - `--plan`: Return the exact proposed outcome without mutation. @@ -399,7 +566,7 @@ Restore exact prior hosted-topology state from a known-good receipt. Operation: mutation. Result schema: `treeseed.platform-operation/v1`. Execution: `local.platform.topology.rollback`. -- `--yes`: Confirm authorized automation. +- `--yes`: Confirm the destructive rollback. - `--json`: Emit the stable JSON envelope. - `--plan`: Return the exact proposed outcome without mutation. @@ -520,6 +687,17 @@ Execution: `local.dev.rebuild`. - `--plan`: Return the exact proposed outcome without mutation. - `--session `: Development session identity. +### trsd dev migrate + +Migrate a local development session. + +Operation: mutation. Result schema: `treeseed.command.dev.migrate/v1`. +Execution: `local.dev.migrate`. + +- `--json`: Emit the stable JSON envelope. +- `--plan`: Return the exact proposed outcome without mutation. +- `--session `: Development session identity. + ### trsd dev restart Restart a local development session. @@ -601,7 +779,7 @@ Operation: mutation. Result schema: `treeseed.host-initialization-result/v1`. Execution: `local.host.initialize`. - `--server `: Control-plane server profile or URL. -- `--yes`: Confirm authorized automation. +- `--yes`: Confirm non-interactive execution after reviewing the plan. - `--json`: Emit the stable JSON envelope. - `--plan`: Return the exact proposed outcome without mutation. - `--input-file `: Team capacity installation configuration downloaded from Admin. Values are never printed. @@ -1317,7 +1495,7 @@ Operation: mutation. Result schema: `treeseed.host-uninstall-result/v1`. Execution: `local.host.uninstall`. - `--server `: Control-plane server profile or URL. -- `--yes`: Confirm authorized automation. +- `--yes`: Confirm non-interactive execution after reviewing the plan. - `--json`: Emit the stable JSON envelope. - `--plan`: Return the exact proposed outcome without mutation. - `--confirm`: Confirm removal of the reviewed TreeSeed resource inventory. @@ -1352,88 +1530,117 @@ Control-plane operation: `agents.show`. - `--project `: Project id or slug. - `--json`: Emit the stable JSON envelope. -### trsd agents validate +## trsd agents team -Validate the selected resource. +Team operations. -Operation: read. Result schema: `treeseed.command.validate/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. +## trsd agents team clone -- `--json`: Emit the stable JSON envelope. +Clone operations. -### trsd agents diff +### trsd agents team clone plan -Diff the selected resource. +Plan the selected resource. -Operation: read. Result schema: `treeseed.command.diff/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. +Operation: read. Result schema: `treeseed.command.plan/v1`. +Control-plane operation: `agents.team.clone.plan`. +- `--server `: Control-plane server profile or URL. +- `--team `: Team id or slug. +- `--project `: Target project; repeat to select projects. - `--json`: Emit the stable JSON envelope. +- `--all`: Target every eligible project except the source. -### trsd agents diagnose +### trsd agents team clone apply -Diagnose the selected resource. +Apply the selected resource. -Operation: read. Result schema: `treeseed.command.diagnose/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. +Operation: mutation. Result schema: `treeseed.command.apply/v1`. +Control-plane operation: `agents.team.clone.apply`. +- `--server `: Control-plane server profile or URL. +- `--team `: Team id or slug. +- `--yes`: Confirm authorized automation. - `--json`: Emit the stable JSON envelope. +- `--idempotency-key `: Reuse the same request identity when retrying this mutation. +- `--plan`: Return the exact proposed outcome without mutation. -## trsd agents classes +## trsd agents handlers -Classes operations. +Handlers operations. -### trsd agents classes list +### trsd agents handlers list List the selected resource. Operation: read. Result schema: `treeseed.command.list/v1`. -Control-plane operation: `agents.classes.list`. +Control-plane operation: `agents.handlers.list`. - `--server `: Control-plane server profile or URL. - `--project `: Project id or slug. - `--json`: Emit the stable JSON envelope. -### trsd agents classes show +### trsd agents handlers show Show the selected resource. Operation: read. Result schema: `treeseed.command.show/v1`. -Control-plane operation: `agents.classes.show`. +Control-plane operation: `agents.handlers.show`. - `--server `: Control-plane server profile or URL. - `--project `: Project id or slug. - `--json`: Emit the stable JSON envelope. -## trsd agents bindings +## trsd agents profiles -Bindings operations. +Profiles operations. -### trsd agents bindings list +### trsd agents profiles show -List the selected resource. +Show the selected resource. -Operation: read. Result schema: `treeseed.command.list/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. +Operation: read. Result schema: `treeseed.command.show/v1`. +Control-plane operation: `agents.show`. +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. - `--json`: Emit the stable JSON envelope. -### trsd agents bindings show +### trsd agents profiles validate -Show the selected resource. +Validate the selected resource. -Operation: read. Result schema: `treeseed.command.show/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. +Operation: read. Result schema: `treeseed.command.validate/v1`. +Control-plane operation: `agents.profiles.validate`. +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. - `--json`: Emit the stable JSON envelope. -### trsd agents bindings explain +## trsd agents classes -Explain the selected resource. +Classes operations. -Operation: read. Result schema: `treeseed.command.explain/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. +### trsd agents classes list + +List the selected resource. + +Operation: read. Result schema: `treeseed.command.list/v1`. +Control-plane operation: `agents.classes.list`. +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. +- `--json`: Emit the stable JSON envelope. + +### trsd agents classes show + +Show the selected resource. + +Operation: read. Result schema: `treeseed.command.show/v1`. +Control-plane operation: `agents.classes.show`. + +- `--server `: Control-plane server profile or URL. +- `--project `: Project id or slug. - `--json`: Emit the stable JSON envelope. ## trsd providers @@ -1861,7 +2068,9 @@ Control-plane operation: `capacity.usage`. - `--server `: Control-plane server profile or URL. - `--team `: Team id or slug. +- `--project `: Project id or slug. - `--json`: Emit the stable JSON envelope. +- `--workday `: Restrict evidence to one workday. ### trsd capacity ledger @@ -1872,10 +2081,12 @@ Control-plane operation: `capacity.ledger`. - `--server `: Control-plane server profile or URL. - `--team `: Team id or slug. +- `--project `: Project id or slug. - `--status `: Status filter. - `--limit `: Page size. - `--cursor `: Opaque page cursor. - `--json`: Emit the stable JSON envelope. +- `--workday `: Restrict evidence to one workday. ### trsd capacity audit @@ -1891,52 +2102,6 @@ Control-plane operation: `capacity.audit`. - `--cursor `: Opaque page cursor. - `--json`: Emit the stable JSON envelope. -## trsd plans - -Plans operations. - -### trsd plans list - -List the selected resource. - -Operation: read. Result schema: `treeseed.command.list/v1`. -Control-plane operation: `plans.list`. - -- `--server `: Control-plane server profile or URL. -- `--decision `: Approved decision identity. -- `--status `: Status filter. -- `--limit `: Page size. -- `--cursor `: Opaque page cursor. -- `--json`: Emit the stable JSON envelope. - -### trsd plans show - -Show the selected resource. - -Operation: read. Result schema: `treeseed.command.show/v1`. -Control-plane operation: `plans.show`. - -- `--server `: Control-plane server profile or URL. -- `--json`: Emit the stable JSON envelope. - -### trsd plans explain - -Explain the selected resource. - -Operation: read. Result schema: `treeseed.command.explain/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. - -- `--json`: Emit the stable JSON envelope. - -### trsd plans diff - -Compare two API-derived plans. - -Operation: read. Result schema: `treeseed.command.plans.diff/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. - -- `--json`: Emit the stable JSON envelope. - ## trsd workdays Workdays operations. @@ -2003,6 +2168,7 @@ Control-plane operation: `workdays.plan`. - `--server `: Control-plane server profile or URL. - `--team `: Team id or slug. - `--profile `: Workday profile identity. +- `--decision `: Accepted decision id; repeat or comma-separate. The API derives and verifies acting authority. - `--projects `: Project scope or comma-separated projects. - `--start `: ISO start time. - `--end `: ISO end time. @@ -2065,48 +2231,19 @@ Availability: fail-closed (`standards_migration_not_enabled`). This capability i - `--json`: Emit the stable JSON envelope. -### trsd workdays pause - -Pause the selected resource. - -Operation: mutation. Result schema: `treeseed.command.pause/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. - -- `--yes`: Confirm authorized automation. -- `--json`: Emit the stable JSON envelope. -- `--plan`: Return the exact proposed outcome without mutation. - -### trsd workdays resume - -Resume the selected resource. - -Operation: mutation. Result schema: `treeseed.command.resume/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. - -- `--yes`: Confirm authorized automation. -- `--json`: Emit the stable JSON envelope. -- `--plan`: Return the exact proposed outcome without mutation. - ### trsd workdays stop Stop the selected resource. Operation: mutation. Result schema: `treeseed.command.stop/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. - -- `--yes`: Confirm authorized automation. -- `--json`: Emit the stable JSON envelope. -- `--plan`: Return the exact proposed outcome without mutation. - -### trsd workdays cancel - -Cancel the selected resource. - -Operation: mutation. Result schema: `treeseed.command.cancel/v1`. -Availability: fail-closed (`standards_migration_not_enabled`). This capability is not enabled until its control-plane operation is accepted. +Control-plane operation: `workdays.stop`. +- `--server `: Control-plane server profile or URL. +- `--team `: Team id or slug. +- `--reason `: Audited operator reason. - `--yes`: Confirm authorized automation. - `--json`: Emit the stable JSON envelope. +- `--idempotency-key `: Reuse the same request identity when retrying this mutation. - `--plan`: Return the exact proposed outcome without mutation. ## trsd workdays schedules @@ -2280,6 +2417,99 @@ Availability: fail-closed (`standards_migration_not_enabled`). This capability i - `--json`: Emit the stable JSON envelope. +## trsd execution + +Execution operations. + +## trsd execution graph + +Graph operations. + +### trsd execution graph show + +Show the selected resource. + +Operation: read. Result schema: `treeseed.command.show/v1`. +Control-plane operation: `execution.graph.show`. + +- `--server `: Control-plane server profile or URL. +- `--team `: Team id or slug. +- `--project `: Filter the team graph by project. +- `--decision `: Filter the team graph by decision. +- `--json`: Emit the stable JSON envelope. + +### trsd execution graph watch + +Watch the selected resource. + +Operation: read. Result schema: `treeseed.command.watch/v1`. +Control-plane operation: `execution.graph.watch`. + +- `--server `: Control-plane server profile or URL. +- `--team `: Team id or slug. +- `--cursor `: Resume after this graph event cursor. +- `--json`: Emit the stable JSON envelope. +- `--wait `: Long-poll duration in seconds. +- `--json-stream`: Emit graph events as NDJSON. + +## trsd execution node + +Node operations. + +### trsd execution node show + +Show the selected resource. + +Operation: read. Result schema: `treeseed.command.show/v1`. +Control-plane operation: `execution.nodes.show`. + +- `--server `: Control-plane server profile or URL. +- `--team `: Team id or slug. +- `--json`: Emit the stable JSON envelope. + +### trsd execution node explain + +Explain the selected resource. + +Operation: read. Result schema: `treeseed.command.explain/v1`. +Control-plane operation: `execution.nodes.explain`. + +- `--server `: Control-plane server profile or URL. +- `--team `: Team id or slug. +- `--json`: Emit the stable JSON envelope. + +### trsd execution reconcile + +Reconcile the selected resource. + +Operation: mutation. Result schema: `treeseed.command.reconcile/v1`. +Control-plane operation: `execution.reconcile`. + +- `--server `: Control-plane server profile or URL. +- `--team `: Team id or slug. +- `--project `: Reconcile one affected project component. +- `--json`: Emit the stable JSON envelope. +- `--idempotency-key `: Reuse the same request identity when retrying this mutation. +- `--plan`: Return the exact proposed outcome without mutation. + +## trsd execution assignments + +Assignments operations. + +### trsd execution assignments list + +List the selected resource. + +Operation: read. Result schema: `treeseed.command.list/v1`. +Control-plane operation: `execution.assignments.list`. + +- `--server `: Control-plane server profile or URL. +- `--team `: Team id or slug. +- `--status `: Filter by assignment status. +- `--limit `: Page size. +- `--cursor `: Opaque page cursor. +- `--json`: Emit the stable JSON envelope. + ## trsd projects Projects operations. @@ -2567,7 +2797,7 @@ Control-plane operation: `ai.instances.storage.put`. - `--server `: Control-plane server profile or URL. - `--team `: Team id or slug. - `--node `: Registered AI node identity. -- `--connection `: Trusted service connection identity. +- `--connection `: Team object-storage service connection ID. - `--json`: Emit the stable JSON envelope. - `--if-match `: Exact current resource version, or new when unconfigured. - `--idempotency-key `: Reuse the same request identity when retrying this mutation. diff --git a/schemas/command-tree.json b/schemas/command-tree.json index 4a701a1..110f591 100644 --- a/schemas/command-tree.json +++ b/schemas/command-tree.json @@ -714,8 +714,8 @@ }, { "nodeType": "branch", - "segment": "secrets", - "description": "Secrets operations.", + "segment": "proposals", + "description": "Proposals operations.", "children": [ { "nodeType": "leaf", @@ -724,26 +724,92 @@ "kind": "read", "resultSchemaId": "treeseed.command.list/v1", "execution": { - "kind": "local", - "handlerId": "local.secrets.list" + "kind": "operation", + "operationId": "governance.proposals.list", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "query", + "field": "status", + "source": "option", + "name": "status", + "required": false, + "transform": "identity" + }, + { + "target": "query", + "field": "limit", + "source": "option", + "name": "limit", + "required": false, + "transform": "integer" + }, + { + "target": "query", + "field": "cursor", + "source": "option", + "name": "cursor", + "required": false, + "transform": "identity" + } + ] } }, { "nodeType": "leaf", - "segment": "status", - "description": "Status the selected resource.", + "segment": "show", + "description": "Show the selected resource.", "kind": "read", - "resultSchemaId": "treeseed.command.status/v1", + "arguments": [ + { + "name": "proposal", + "description": "proposal identity or path.", + "required": true + } + ], + "resultSchemaId": "treeseed.command.show/v1", "execution": { - "kind": "local", - "handlerId": "local.secrets.status" + "kind": "operation", + "operationId": "governance.proposals.show", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "proposalId", + "source": "argument", + "name": "proposal", + "required": true, + "transform": "identity" + } + ] } }, { "nodeType": "leaf", - "segment": "unlock", - "description": "Unlock the selected resource.", + "segment": "create", + "description": "Create the selected resource.", "kind": "mutation", + "arguments": [ + { + "name": "file", + "description": "file identity or path.", + "required": true + } + ], "options": [ { "name": "--plan", @@ -752,267 +818,350 @@ } ], "authorization": { - "capability": "command.unlock", - "confirmation": "credential" + "capability": "command.create", + "confirmation": "never" }, - "resultSchemaId": "treeseed.command.unlock/v1", + "resultSchemaId": "treeseed.command.create/v1", "execution": { - "kind": "local", - "handlerId": "local.secrets.unlock" + "kind": "operation", + "operationId": "governance.proposals.create", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "file", + "source": "argument", + "name": "file", + "required": true, + "transform": "identity" + } + ] } }, { "nodeType": "leaf", - "segment": "lock", - "description": "Lock the selected resource.", + "segment": "update", + "description": "Update the selected resource.", "kind": "mutation", + "arguments": [ + { + "name": "proposal", + "description": "proposal identity or path.", + "required": true + } + ], "options": [ { "name": "--plan", "description": "Return the exact proposed outcome without mutation.", "type": "boolean" + }, + { + "name": "--input", + "description": "YAML or JSON proposal update.", + "type": "string", + "required": true } ], "authorization": { - "capability": "command.lock", + "capability": "command.update", "confirmation": "never" }, - "resultSchemaId": "treeseed.command.lock/v1", + "resultSchemaId": "treeseed.command.update/v1", "execution": { - "kind": "local", - "handlerId": "local.secrets.lock" + "kind": "operation", + "operationId": "governance.proposals.update", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "proposalId", + "source": "argument", + "name": "proposal", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "file", + "source": "option", + "name": "input", + "required": true, + "transform": "identity" + } + ] } - } - ] - }, - { - "nodeType": "branch", - "segment": "services", - "description": "Team service credentials.", - "children": [ + }, { - "nodeType": "branch", - "segment": "credentials", - "description": "Manage credentials in core OpenBao.", - "children": [ + "nodeType": "leaf", + "segment": "open", + "description": "Open the selected resource.", + "kind": "mutation", + "arguments": [ { - "nodeType": "leaf", - "segment": "show", - "description": "show an exact service credential profile.", - "kind": "read", - "arguments": [ - { - "name": "connection", - "description": "Service connection ID.", - "required": true - }, - { - "name": "profile", - "description": "Credential profile ID.", - "required": true - } - ], - "options": [ - { - "name": "--team", - "description": "Authorized team ID.", - "type": "string" - } - ], - "authorization": { - "capability": "secrets.read", - "confirmation": "never" + "name": "proposal", + "description": "proposal identity or path.", + "required": true + } + ], + "options": [ + { + "name": "--plan", + "description": "Return the exact proposed outcome without mutation.", + "type": "boolean" + } + ], + "authorization": { + "capability": "command.open", + "confirmation": "never" + }, + "resultSchemaId": "treeseed.command.open/v1", + "execution": { + "kind": "operation", + "operationId": "governance.proposals.open", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" }, - "resultSchemaId": "treeseed.services.credentials.show/v1", - "execution": { - "kind": "local", - "handlerId": "local.services.credentials.show" + { + "target": "path", + "field": "proposalId", + "source": "argument", + "name": "proposal", + "required": true, + "transform": "identity" } - }, + ] + } + }, + { + "nodeType": "branch", + "segment": "feedback", + "description": "Feedback operations.", + "children": [ { "nodeType": "leaf", - "segment": "put", - "description": "put an exact service credential profile.", + "segment": "resolve", + "description": "Resolve the selected resource.", "kind": "mutation", "arguments": [ { - "name": "connection", - "description": "Service connection ID.", - "required": true - }, - { - "name": "profile", - "description": "Credential profile ID.", + "name": "proposal", + "description": "proposal identity or path.", "required": true } ], "options": [ - { - "name": "--team", - "description": "Authorized team ID.", - "type": "string" - }, { "name": "--plan", - "description": "Inspect the proposed operation without mutation or secret input.", + "description": "Return the exact proposed outcome without mutation.", "type": "boolean" }, { - "name": "--expected-version", - "description": "Exact current credential version; zero for first creation.", - "type": "number", + "name": "--feedback", + "description": "Exact blocking feedback event identity.", + "type": "string", "required": true }, { - "name": "--stdin", - "description": "Read the credential field object as JSON from standard input.", - "type": "boolean" + "name": "--input", + "description": "YAML or JSON resolution evidence.", + "type": "string", + "required": true } ], "authorization": { - "capability": "secrets.write", + "capability": "command.resolve", "confirmation": "never" }, - "resultSchemaId": "treeseed.services.credentials.put/v1", + "resultSchemaId": "treeseed.command.resolve/v1", "execution": { - "kind": "local", - "handlerId": "local.services.credentials.put" - } - }, - { - "nodeType": "leaf", - "segment": "delete", - "description": "delete an exact service credential profile.", - "kind": "mutation", - "arguments": [ - { - "name": "connection", - "description": "Service connection ID.", - "required": true - }, - { - "name": "profile", - "description": "Credential profile ID.", - "required": true - } - ], - "options": [ - { - "name": "--team", - "description": "Authorized team ID.", - "type": "string" - }, - { - "name": "--plan", - "description": "Inspect the proposed operation without mutation or secret input.", - "type": "boolean" - }, - { - "name": "--expected-version", - "description": "Exact current credential version; zero for first creation.", - "type": "number", - "required": true - } - ], - "authorization": { - "capability": "secrets.write", - "confirmation": "never" - }, - "resultSchemaId": "treeseed.services.credentials.delete/v1", - "execution": { - "kind": "local", - "handlerId": "local.services.credentials.delete" + "kind": "operation", + "operationId": "governance.proposals.feedback.resolve", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "proposalId", + "source": "argument", + "name": "proposal", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "feedbackId", + "source": "option", + "name": "feedback", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "file", + "source": "option", + "name": "input", + "required": true, + "transform": "identity" + } + ] } - }, + } + ] + }, + { + "nodeType": "branch", + "segment": "voting", + "description": "Voting operations.", + "children": [ { "nodeType": "leaf", - "segment": "validate", - "description": "validate an exact service credential profile.", + "segment": "start", + "description": "Start the selected resource.", "kind": "mutation", "arguments": [ { - "name": "connection", - "description": "Service connection ID.", - "required": true - }, - { - "name": "profile", - "description": "Credential profile ID.", + "name": "proposal", + "description": "proposal identity or path.", "required": true } ], "options": [ - { - "name": "--team", - "description": "Authorized team ID.", - "type": "string" - }, { "name": "--plan", - "description": "Inspect the proposed operation without mutation or secret input.", + "description": "Return the exact proposed outcome without mutation.", "type": "boolean" - }, - { - "name": "--expected-version", - "description": "Exact current credential version; zero for first creation.", - "type": "number", - "required": true } ], "authorization": { - "capability": "secrets.write", + "capability": "command.start", "confirmation": "never" }, - "resultSchemaId": "treeseed.services.credentials.validate/v1", + "resultSchemaId": "treeseed.command.start/v1", "execution": { - "kind": "local", - "handlerId": "local.services.credentials.validate" + "kind": "operation", + "operationId": "governance.proposals.voting.start", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "proposalId", + "source": "argument", + "name": "proposal", + "required": true, + "transform": "identity" + } + ] } } ] - } - ] - }, - { - "nodeType": "branch", - "segment": "platform", - "description": "Platform operations.", - "children": [ + }, { "nodeType": "leaf", - "segment": "verify", - "description": "Verify a declarative Platform repository without package checkouts or a control-plane API.", - "kind": "read", + "segment": "vote", + "description": "Vote the selected resource.", + "kind": "mutation", "arguments": [ { - "name": "root", - "description": "Platform root; defaults to the current directory.", - "required": false + "name": "proposal", + "description": "proposal identity or path.", + "required": true } ], "options": [ { - "name": "--profile", - "description": "Composable profile to verify.", - "type": "string[]" + "name": "--plan", + "description": "Return the exact proposed outcome without mutation.", + "type": "boolean" }, { - "name": "--json", - "description": "Emit the stable command-result envelope.", - "type": "boolean" + "name": "--input", + "description": "YAML or JSON vote.", + "type": "string", + "required": true } ], - "resultSchemaId": "treeseed.platform-verification/v1", + "authorization": { + "capability": "command.vote", + "confirmation": "never" + }, + "resultSchemaId": "treeseed.command.vote/v1", "execution": { - "kind": "local", - "handlerId": "local.platform.verify" + "kind": "operation", + "operationId": "governance.proposals.vote", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "proposalId", + "source": "argument", + "name": "proposal", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "file", + "source": "option", + "name": "input", + "required": true, + "transform": "identity" + } + ] } }, { "nodeType": "leaf", - "segment": "workset", - "description": "Plan or safely materialize exact primary source checkouts beneath packages/.", + "segment": "evaluate", + "description": "Evaluate the selected resource.", "kind": "mutation", + "arguments": [ + { + "name": "proposal", + "description": "proposal identity or path.", + "required": true + } + ], "options": [ { "name": "--plan", @@ -1020,44 +1169,483 @@ "type": "boolean" }, { - "name": "--apply", - "description": "Apply the frozen workset plan.", - "type": "boolean" - }, - { - "name": "--yes", - "description": "Confirm the planned checkout mutations.", - "type": "boolean" - }, - { - "name": "--profile", - "description": "Composable source profile; repeat to union profiles.", - "type": "string[]" - }, - { - "name": "--project", - "description": "Explicit project slug; repeat to select projects.", - "type": "string[]" - }, - { - "name": "--exclude", - "description": "Project slug to exclude; repeat as needed.", - "type": "string[]" - }, - { - "name": "--json", - "description": "Emit the stable command-result envelope.", - "type": "boolean" + "name": "--input", + "description": "Optional YAML or JSON evaluation decision.", + "type": "string" } ], "authorization": { - "capability": "development.workset", + "capability": "command.evaluate", "confirmation": "never" }, - "resultSchemaId": "treeseed.platform-workset-result/v1", + "resultSchemaId": "treeseed.command.evaluate/v1", "execution": { - "kind": "local", - "handlerId": "local.platform.workset" + "kind": "operation", + "operationId": "governance.proposals.evaluate", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "proposalId", + "source": "argument", + "name": "proposal", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "file", + "source": "option", + "name": "input", + "required": false, + "transform": "identity" + } + ] + } + } + ] + }, + { + "nodeType": "branch", + "segment": "decisions", + "description": "Decisions operations.", + "children": [ + { + "nodeType": "leaf", + "segment": "list", + "description": "List the selected resource.", + "kind": "read", + "resultSchemaId": "treeseed.command.list/v1", + "execution": { + "kind": "operation", + "operationId": "governance.decisions.list", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "query", + "field": "status", + "source": "option", + "name": "status", + "required": false, + "transform": "identity" + }, + { + "target": "query", + "field": "limit", + "source": "option", + "name": "limit", + "required": false, + "transform": "integer" + }, + { + "target": "query", + "field": "cursor", + "source": "option", + "name": "cursor", + "required": false, + "transform": "identity" + } + ] + } + }, + { + "nodeType": "leaf", + "segment": "show", + "description": "Show the selected resource.", + "kind": "read", + "arguments": [ + { + "name": "decision", + "description": "decision identity or path.", + "required": true + } + ], + "resultSchemaId": "treeseed.command.show/v1", + "execution": { + "kind": "operation", + "operationId": "governance.decisions.show", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "decisionId", + "source": "argument", + "name": "decision", + "required": true, + "transform": "identity" + } + ] + } + } + ] + }, + { + "nodeType": "branch", + "segment": "secrets", + "description": "Secrets operations.", + "children": [ + { + "nodeType": "leaf", + "segment": "list", + "description": "List the selected resource.", + "kind": "read", + "resultSchemaId": "treeseed.command.list/v1", + "execution": { + "kind": "local", + "handlerId": "local.secrets.list" + } + }, + { + "nodeType": "leaf", + "segment": "status", + "description": "Status the selected resource.", + "kind": "read", + "resultSchemaId": "treeseed.command.status/v1", + "execution": { + "kind": "local", + "handlerId": "local.secrets.status" + } + }, + { + "nodeType": "leaf", + "segment": "unlock", + "description": "Unlock the selected resource.", + "kind": "mutation", + "options": [ + { + "name": "--plan", + "description": "Return the exact proposed outcome without mutation.", + "type": "boolean" + } + ], + "authorization": { + "capability": "command.unlock", + "confirmation": "credential" + }, + "resultSchemaId": "treeseed.command.unlock/v1", + "execution": { + "kind": "local", + "handlerId": "local.secrets.unlock" + } + }, + { + "nodeType": "leaf", + "segment": "lock", + "description": "Lock the selected resource.", + "kind": "mutation", + "options": [ + { + "name": "--plan", + "description": "Return the exact proposed outcome without mutation.", + "type": "boolean" + } + ], + "authorization": { + "capability": "command.lock", + "confirmation": "never" + }, + "resultSchemaId": "treeseed.command.lock/v1", + "execution": { + "kind": "local", + "handlerId": "local.secrets.lock" + } + } + ] + }, + { + "nodeType": "branch", + "segment": "services", + "description": "Team service credentials.", + "children": [ + { + "nodeType": "branch", + "segment": "credentials", + "description": "Manage credentials in core OpenBao.", + "children": [ + { + "nodeType": "leaf", + "segment": "show", + "description": "show an exact service credential profile.", + "kind": "read", + "arguments": [ + { + "name": "connection", + "description": "Service connection ID.", + "required": true + }, + { + "name": "profile", + "description": "Credential profile ID.", + "required": true + } + ], + "options": [ + { + "name": "--team", + "description": "Authorized team ID.", + "type": "string" + } + ], + "authorization": { + "capability": "secrets.read", + "confirmation": "never" + }, + "resultSchemaId": "treeseed.services.credentials.show/v1", + "execution": { + "kind": "local", + "handlerId": "local.services.credentials.show" + } + }, + { + "nodeType": "leaf", + "segment": "put", + "description": "put an exact service credential profile.", + "kind": "mutation", + "arguments": [ + { + "name": "connection", + "description": "Service connection ID.", + "required": true + }, + { + "name": "profile", + "description": "Credential profile ID.", + "required": true + } + ], + "options": [ + { + "name": "--team", + "description": "Authorized team ID.", + "type": "string" + }, + { + "name": "--plan", + "description": "Inspect the proposed operation without mutation or secret input.", + "type": "boolean" + }, + { + "name": "--expected-version", + "description": "Exact current credential version; zero for first creation.", + "type": "number", + "required": true + }, + { + "name": "--stdin", + "description": "Read the credential field object as JSON from standard input.", + "type": "boolean" + } + ], + "authorization": { + "capability": "secrets.write", + "confirmation": "never" + }, + "resultSchemaId": "treeseed.services.credentials.put/v1", + "execution": { + "kind": "local", + "handlerId": "local.services.credentials.put" + } + }, + { + "nodeType": "leaf", + "segment": "delete", + "description": "delete an exact service credential profile.", + "kind": "mutation", + "arguments": [ + { + "name": "connection", + "description": "Service connection ID.", + "required": true + }, + { + "name": "profile", + "description": "Credential profile ID.", + "required": true + } + ], + "options": [ + { + "name": "--team", + "description": "Authorized team ID.", + "type": "string" + }, + { + "name": "--plan", + "description": "Inspect the proposed operation without mutation or secret input.", + "type": "boolean" + }, + { + "name": "--expected-version", + "description": "Exact current credential version; zero for first creation.", + "type": "number", + "required": true + } + ], + "authorization": { + "capability": "secrets.write", + "confirmation": "never" + }, + "resultSchemaId": "treeseed.services.credentials.delete/v1", + "execution": { + "kind": "local", + "handlerId": "local.services.credentials.delete" + } + }, + { + "nodeType": "leaf", + "segment": "validate", + "description": "validate an exact service credential profile.", + "kind": "mutation", + "arguments": [ + { + "name": "connection", + "description": "Service connection ID.", + "required": true + }, + { + "name": "profile", + "description": "Credential profile ID.", + "required": true + } + ], + "options": [ + { + "name": "--team", + "description": "Authorized team ID.", + "type": "string" + }, + { + "name": "--plan", + "description": "Inspect the proposed operation without mutation or secret input.", + "type": "boolean" + }, + { + "name": "--expected-version", + "description": "Exact current credential version; zero for first creation.", + "type": "number", + "required": true + } + ], + "authorization": { + "capability": "secrets.write", + "confirmation": "never" + }, + "resultSchemaId": "treeseed.services.credentials.validate/v1", + "execution": { + "kind": "local", + "handlerId": "local.services.credentials.validate" + } + } + ] + } + ] + }, + { + "nodeType": "branch", + "segment": "platform", + "description": "Platform operations.", + "children": [ + { + "nodeType": "leaf", + "segment": "verify", + "description": "Verify a declarative Platform repository without package checkouts or a control-plane API.", + "kind": "read", + "arguments": [ + { + "name": "root", + "description": "Platform root; defaults to the current directory.", + "required": false + } + ], + "options": [ + { + "name": "--profile", + "description": "Composable profile to verify.", + "type": "string[]" + }, + { + "name": "--json", + "description": "Emit the stable command-result envelope.", + "type": "boolean" + } + ], + "resultSchemaId": "treeseed.platform-verification/v1", + "execution": { + "kind": "local", + "handlerId": "local.platform.verify" + } + }, + { + "nodeType": "leaf", + "segment": "workset", + "description": "Plan or safely materialize exact primary source checkouts beneath packages/.", + "kind": "mutation", + "options": [ + { + "name": "--plan", + "description": "Return the exact proposed outcome without mutation.", + "type": "boolean" + }, + { + "name": "--apply", + "description": "Apply the frozen workset plan.", + "type": "boolean" + }, + { + "name": "--yes", + "description": "Confirm the planned checkout mutations.", + "type": "boolean" + }, + { + "name": "--profile", + "description": "Composable source profile; repeat to union profiles.", + "type": "string[]" + }, + { + "name": "--project", + "description": "Explicit project slug; repeat to select projects.", + "type": "string[]" + }, + { + "name": "--exclude", + "description": "Project slug to exclude; repeat as needed.", + "type": "string[]" + }, + { + "name": "--json", + "description": "Emit the stable command-result envelope.", + "type": "boolean" + } + ], + "authorization": { + "capability": "development.workset", + "confirmation": "never" + }, + "resultSchemaId": "treeseed.platform-workset-result/v1", + "execution": { + "kind": "local", + "handlerId": "local.platform.workset" } }, { @@ -1480,27 +2068,61 @@ "name": "--session", "description": "Development session identity.", "type": "string" - }, - { - "name": "--target", - "description": "Additional project.target=mode selections.", - "type": "string[]" + }, + { + "name": "--target", + "description": "Additional project.target=mode selections.", + "type": "string[]" + } + ], + "authorization": { + "capability": "development.use", + "confirmation": "never" + }, + "resultSchemaId": "treeseed.command.dev.use/v1", + "execution": { + "kind": "local", + "handlerId": "local.dev.use" + } + }, + { + "nodeType": "leaf", + "segment": "rebuild", + "description": "Rebuild a local development session.", + "kind": "mutation", + "arguments": [ + { + "name": "target", + "description": "target value.", + "required": true + } + ], + "options": [ + { + "name": "--plan", + "description": "Return the exact proposed outcome without mutation.", + "type": "boolean" + }, + { + "name": "--session", + "description": "Development session identity.", + "type": "string" } ], "authorization": { - "capability": "development.use", + "capability": "development.rebuild", "confirmation": "never" }, - "resultSchemaId": "treeseed.command.dev.use/v1", + "resultSchemaId": "treeseed.command.dev.rebuild/v1", "execution": { "kind": "local", - "handlerId": "local.dev.use" + "handlerId": "local.dev.rebuild" } }, { "nodeType": "leaf", - "segment": "rebuild", - "description": "Rebuild a local development session.", + "segment": "migrate", + "description": "Migrate a local development session.", "kind": "mutation", "arguments": [ { @@ -1522,13 +2144,13 @@ } ], "authorization": { - "capability": "development.rebuild", + "capability": "development.migrate", "confirmation": "never" }, - "resultSchemaId": "treeseed.command.dev.rebuild/v1", + "resultSchemaId": "treeseed.command.dev.migrate/v1", "execution": { "kind": "local", - "handlerId": "local.dev.rebuild" + "handlerId": "local.dev.migrate" } }, { @@ -3157,45 +3779,134 @@ } }, { - "nodeType": "leaf", - "segment": "validate", - "description": "Validate the selected resource.", - "kind": "read", - "resultSchemaId": "treeseed.command.validate/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." - } - }, - { - "nodeType": "leaf", - "segment": "diff", - "description": "Diff the selected resource.", - "kind": "read", - "resultSchemaId": "treeseed.command.diff/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." - } - }, - { - "nodeType": "leaf", - "segment": "diagnose", - "description": "Diagnose the selected resource.", - "kind": "read", - "resultSchemaId": "treeseed.command.diagnose/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." - } + "nodeType": "branch", + "segment": "team", + "description": "Team operations.", + "children": [ + { + "nodeType": "branch", + "segment": "clone", + "description": "Clone operations.", + "children": [ + { + "nodeType": "leaf", + "segment": "plan", + "description": "Plan the selected resource.", + "kind": "read", + "arguments": [ + { + "name": "source", + "description": "source identity or path.", + "required": true + } + ], + "options": [ + { + "name": "--project", + "description": "Target project; repeat to select projects.", + "type": "string[]" + }, + { + "name": "--all", + "description": "Target every eligible project except the source.", + "type": "boolean" + } + ], + "resultSchemaId": "treeseed.command.plan/v1", + "execution": { + "kind": "operation", + "operationId": "agents.team.clone.plan", + "input": [ + { + "target": "path", + "field": "teamId", + "source": "context", + "name": "team", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "sourceProject", + "source": "argument", + "name": "source", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "targetProjects", + "source": "option", + "name": "project", + "required": false, + "transform": "identity" + }, + { + "target": "body", + "field": "allEligible", + "source": "option", + "name": "all", + "required": false, + "transform": "identity" + } + ] + } + }, + { + "nodeType": "leaf", + "segment": "apply", + "description": "Apply the selected resource.", + "kind": "mutation", + "arguments": [ + { + "name": "file", + "description": "file identity or path.", + "required": true + } + ], + "options": [ + { + "name": "--plan", + "description": "Return the exact proposed outcome without mutation.", + "type": "boolean" + } + ], + "authorization": { + "capability": "command.apply", + "confirmation": "authority" + }, + "resultSchemaId": "treeseed.command.apply/v1", + "execution": { + "kind": "operation", + "operationId": "agents.team.clone.apply", + "input": [ + { + "target": "path", + "field": "teamId", + "source": "context", + "name": "team", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "file", + "source": "argument", + "name": "file", + "required": true, + "transform": "identity" + } + ] + } + } + ] + } + ] }, { "nodeType": "branch", - "segment": "classes", - "description": "Classes operations.", + "segment": "handlers", + "description": "Handlers operations.", "children": [ { "nodeType": "leaf", @@ -3205,7 +3916,7 @@ "resultSchemaId": "treeseed.command.list/v1", "execution": { "kind": "operation", - "operationId": "agents.classes.list", + "operationId": "agents.handlers.list", "input": [ { "target": "path", @@ -3225,15 +3936,15 @@ "kind": "read", "arguments": [ { - "name": "class", - "description": "class identity or path.", + "name": "handler", + "description": "handler identity or path.", "required": true } ], "resultSchemaId": "treeseed.command.show/v1", "execution": { "kind": "operation", - "operationId": "agents.classes.show", + "operationId": "agents.handlers.show", "input": [ { "target": "path", @@ -3245,9 +3956,9 @@ }, { "target": "path", - "field": "classId", + "field": "handlerId", "source": "argument", - "name": "class", + "name": "handler", "required": true, "transform": "identity" } @@ -3258,57 +3969,143 @@ }, { "nodeType": "branch", - "segment": "bindings", - "description": "Bindings operations.", + "segment": "profiles", + "description": "Profiles operations.", "children": [ { "nodeType": "leaf", - "segment": "list", - "description": "List the selected resource.", + "segment": "show", + "description": "Show the selected resource.", "kind": "read", - "resultSchemaId": "treeseed.command.list/v1", + "arguments": [ + { + "name": "profile", + "description": "profile identity or path.", + "required": true + } + ], + "resultSchemaId": "treeseed.command.show/v1", "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." + "kind": "operation", + "operationId": "agents.show", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "agentSlug", + "source": "argument", + "name": "profile", + "required": true, + "transform": "identity" + } + ] } }, { "nodeType": "leaf", - "segment": "show", - "description": "Show the selected resource.", + "segment": "validate", + "description": "Validate the selected resource.", "kind": "read", "arguments": [ { - "name": "binding", - "description": "binding identity or path.", + "name": "profile", + "description": "profile identity or path.", "required": true } ], - "resultSchemaId": "treeseed.command.show/v1", + "resultSchemaId": "treeseed.command.validate/v1", "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." + "kind": "operation", + "operationId": "agents.profiles.validate", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "agentSlug", + "source": "argument", + "name": "profile", + "required": true, + "transform": "identity" + } + ] + } + } + ] + }, + { + "nodeType": "branch", + "segment": "classes", + "description": "Classes operations.", + "children": [ + { + "nodeType": "leaf", + "segment": "list", + "description": "List the selected resource.", + "kind": "read", + "resultSchemaId": "treeseed.command.list/v1", + "execution": { + "kind": "operation", + "operationId": "agents.classes.list", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + } + ] } }, { "nodeType": "leaf", - "segment": "explain", - "description": "Explain the selected resource.", + "segment": "show", + "description": "Show the selected resource.", "kind": "read", "arguments": [ { - "name": "binding", - "description": "binding identity or path.", + "name": "class", + "description": "class identity or path.", "required": true } ], - "resultSchemaId": "treeseed.command.explain/v1", + "resultSchemaId": "treeseed.command.show/v1", "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." + "kind": "operation", + "operationId": "agents.classes.show", + "input": [ + { + "target": "path", + "field": "projectId", + "source": "context", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "classId", + "source": "argument", + "name": "class", + "required": true, + "transform": "identity" + } + ] } } ] @@ -4545,6 +5342,13 @@ "segment": "usage", "description": "Usage the selected resource.", "kind": "read", + "options": [ + { + "name": "--workday", + "description": "Restrict evidence to one workday.", + "type": "string" + } + ], "resultSchemaId": "treeseed.command.usage/v1", "execution": { "kind": "operation", @@ -4557,6 +5361,22 @@ "name": "team", "required": true, "transform": "identity" + }, + { + "target": "query", + "field": "projectId", + "source": "option", + "name": "project", + "required": true, + "transform": "identity" + }, + { + "target": "query", + "field": "workDayId", + "source": "option", + "name": "workday", + "required": false, + "transform": "identity" } ] } @@ -4566,6 +5386,13 @@ "segment": "ledger", "description": "Ledger the selected resource.", "kind": "read", + "options": [ + { + "name": "--workday", + "description": "Restrict evidence to one workday.", + "type": "string" + } + ], "resultSchemaId": "treeseed.command.ledger/v1", "execution": { "kind": "operation", @@ -4581,48 +5408,19 @@ }, { "target": "query", - "field": "status", + "field": "projectId", "source": "option", - "name": "status", - "required": false, + "name": "project", + "required": true, "transform": "identity" }, { "target": "query", - "field": "limit", - "source": "option", - "name": "limit", - "required": false, - "transform": "integer" - }, - { - "target": "query", - "field": "cursor", + "field": "workDayId", "source": "option", - "name": "cursor", + "name": "workday", "required": false, "transform": "identity" - } - ] - } - }, - { - "nodeType": "leaf", - "segment": "audit", - "description": "Audit the selected resource.", - "kind": "read", - "resultSchemaId": "treeseed.command.audit/v1", - "execution": { - "kind": "operation", - "operationId": "capacity.audit", - "input": [ - { - "target": "path", - "field": "teamId", - "source": "context", - "name": "team", - "required": true, - "transform": "identity" }, { "target": "query", @@ -4650,29 +5448,22 @@ } ] } - } - ] - }, - { - "nodeType": "branch", - "segment": "plans", - "description": "Plans operations.", - "children": [ + }, { "nodeType": "leaf", - "segment": "list", - "description": "List the selected resource.", + "segment": "audit", + "description": "Audit the selected resource.", "kind": "read", - "resultSchemaId": "treeseed.command.list/v1", + "resultSchemaId": "treeseed.command.audit/v1", "execution": { "kind": "operation", - "operationId": "plans.list", + "operationId": "capacity.audit", "input": [ { "target": "path", - "field": "decisionId", - "source": "option", - "name": "decision", + "field": "teamId", + "source": "context", + "name": "team", "required": true, "transform": "identity" }, @@ -4702,77 +5493,6 @@ } ] } - }, - { - "nodeType": "leaf", - "segment": "show", - "description": "Show the selected resource.", - "kind": "read", - "arguments": [ - { - "name": "plan", - "description": "plan identity or path.", - "required": true - } - ], - "resultSchemaId": "treeseed.command.show/v1", - "execution": { - "kind": "operation", - "operationId": "plans.show", - "input": [ - { - "target": "path", - "field": "capacityPlanId", - "source": "argument", - "name": "plan", - "required": true, - "transform": "identity" - } - ] - } - }, - { - "nodeType": "leaf", - "segment": "explain", - "description": "Explain the selected resource.", - "kind": "read", - "arguments": [ - { - "name": "plan", - "description": "plan identity or path.", - "required": true - } - ], - "resultSchemaId": "treeseed.command.explain/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." - } - }, - { - "nodeType": "leaf", - "segment": "diff", - "description": "Compare two API-derived plans.", - "kind": "read", - "arguments": [ - { - "name": "left", - "description": "Left plan identity.", - "required": true - }, - { - "name": "right", - "description": "Right plan identity.", - "required": true - } - ], - "resultSchemaId": "treeseed.command.plans.diff/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." - } } ] }, @@ -4943,6 +5663,11 @@ "description": "Return the request without creating a preflight.", "type": "boolean" }, + { + "name": "--decision", + "description": "Accepted decision id; repeat or comma-separate. The API derives and verifies acting authority.", + "type": "string[]" + }, { "name": "--agent", "description": "Planning agent slug; repeat or comma-separate. Intersects with class/activity selectors.", @@ -5024,6 +5749,14 @@ "required": false, "transform": "csv" }, + { + "target": "body", + "field": "decisionIds", + "source": "option", + "name": "decision", + "required": false, + "transform": "csv" + }, { "target": "body", "field": "agentSelection.agentSlugs", @@ -5196,66 +5929,6 @@ "reason": "This capability is not enabled until its control-plane operation is accepted." } }, - { - "nodeType": "leaf", - "segment": "pause", - "description": "Pause the selected resource.", - "kind": "mutation", - "arguments": [ - { - "name": "workday", - "description": "workday identity or path.", - "required": true - } - ], - "options": [ - { - "name": "--plan", - "description": "Return the exact proposed outcome without mutation.", - "type": "boolean" - } - ], - "authorization": { - "capability": "command.pause", - "confirmation": "authority" - }, - "resultSchemaId": "treeseed.command.pause/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." - } - }, - { - "nodeType": "leaf", - "segment": "resume", - "description": "Resume the selected resource.", - "kind": "mutation", - "arguments": [ - { - "name": "workday", - "description": "workday identity or path.", - "required": true - } - ], - "options": [ - { - "name": "--plan", - "description": "Return the exact proposed outcome without mutation.", - "type": "boolean" - } - ], - "authorization": { - "capability": "command.resume", - "confirmation": "authority" - }, - "resultSchemaId": "treeseed.command.resume/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." - } - }, { "nodeType": "leaf", "segment": "stop", @@ -5277,43 +5950,38 @@ ], "authorization": { "capability": "command.stop", - "confirmation": "destructive" + "confirmation": "authority" }, "resultSchemaId": "treeseed.command.stop/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." - } - }, - { - "nodeType": "leaf", - "segment": "cancel", - "description": "Cancel the selected resource.", - "kind": "mutation", - "arguments": [ - { - "name": "workday", - "description": "workday identity or path.", - "required": true - } - ], - "options": [ - { - "name": "--plan", - "description": "Return the exact proposed outcome without mutation.", - "type": "boolean" - } - ], - "authorization": { - "capability": "command.cancel", - "confirmation": "destructive" - }, - "resultSchemaId": "treeseed.command.cancel/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." + "execution": { + "kind": "operation", + "operationId": "workdays.stop", + "input": [ + { + "target": "path", + "field": "teamId", + "source": "context", + "name": "team", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "runId", + "source": "argument", + "name": "workday", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "reason", + "source": "option", + "name": "reason", + "required": false, + "transform": "identity" + } + ] } }, { @@ -5642,55 +6310,378 @@ "required": true, "transform": "identity" } - ] - } + ] + } + }, + { + "nodeType": "leaf", + "segment": "watch", + "description": "Watch the selected resource.", + "kind": "read", + "arguments": [ + { + "name": "assignment", + "description": "assignment identity or path.", + "required": true + } + ], + "resultSchemaId": "treeseed.command.watch/v1", + "execution": { + "kind": "unavailable", + "code": "standards_migration_not_enabled", + "reason": "This capability is not enabled until its control-plane operation is accepted." + } + }, + { + "nodeType": "leaf", + "segment": "retry", + "description": "Retry the selected resource.", + "kind": "mutation", + "arguments": [ + { + "name": "assignment", + "description": "assignment identity or path.", + "required": true + } + ], + "options": [ + { + "name": "--plan", + "description": "Return the exact proposed outcome without mutation.", + "type": "boolean" + } + ], + "authorization": { + "capability": "command.retry", + "confirmation": "authority" + }, + "resultSchemaId": "treeseed.command.retry/v1", + "execution": { + "kind": "operation", + "operationId": "assignments.retry", + "input": [ + { + "target": "path", + "field": "teamId", + "source": "context", + "name": "team", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "assignmentId", + "source": "argument", + "name": "assignment", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "reason", + "source": "option", + "name": "reason", + "required": false, + "transform": "identity" + } + ] + } + }, + { + "nodeType": "leaf", + "segment": "cancel", + "description": "Cancel the selected resource.", + "kind": "mutation", + "arguments": [ + { + "name": "assignment", + "description": "assignment identity or path.", + "required": true + } + ], + "options": [ + { + "name": "--plan", + "description": "Return the exact proposed outcome without mutation.", + "type": "boolean" + } + ], + "authorization": { + "capability": "command.cancel", + "confirmation": "destructive" + }, + "resultSchemaId": "treeseed.command.cancel/v1", + "execution": { + "kind": "operation", + "operationId": "assignments.cancel", + "input": [ + { + "target": "path", + "field": "teamId", + "source": "context", + "name": "team", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "assignmentId", + "source": "argument", + "name": "assignment", + "required": true, + "transform": "identity" + }, + { + "target": "body", + "field": "reason", + "source": "option", + "name": "reason", + "required": false, + "transform": "identity" + } + ] + } + }, + { + "nodeType": "leaf", + "segment": "artifacts", + "description": "Artifacts the selected resource.", + "kind": "read", + "arguments": [ + { + "name": "assignment", + "description": "assignment identity or path.", + "required": true + } + ], + "resultSchemaId": "treeseed.command.artifacts/v1", + "execution": { + "kind": "unavailable", + "code": "standards_migration_not_enabled", + "reason": "This capability is not enabled until its control-plane operation is accepted." + } + } + ] + }, + { + "nodeType": "branch", + "segment": "execution", + "description": "Execution operations.", + "children": [ + { + "nodeType": "branch", + "segment": "graph", + "description": "Graph operations.", + "children": [ + { + "nodeType": "leaf", + "segment": "show", + "description": "Show the selected resource.", + "kind": "read", + "options": [ + { + "name": "--project", + "description": "Filter the team graph by project.", + "type": "string" + }, + { + "name": "--decision", + "description": "Filter the team graph by decision.", + "type": "string" + } + ], + "resultSchemaId": "treeseed.command.show/v1", + "execution": { + "kind": "operation", + "operationId": "execution.graph.show", + "input": [ + { + "target": "path", + "field": "teamId", + "source": "context", + "name": "team", + "required": true, + "transform": "identity" + }, + { + "target": "query", + "field": "projectId", + "source": "option", + "name": "project", + "required": false, + "transform": "identity" + }, + { + "target": "query", + "field": "decisionId", + "source": "option", + "name": "decision", + "required": false, + "transform": "identity" + } + ] + } + }, + { + "nodeType": "leaf", + "segment": "watch", + "description": "Watch the selected resource.", + "kind": "read", + "options": [ + { + "name": "--cursor", + "description": "Resume after this graph event cursor.", + "type": "string" + }, + { + "name": "--wait", + "description": "Long-poll duration in seconds.", + "type": "number" + }, + { + "name": "--json-stream", + "description": "Emit graph events as NDJSON.", + "type": "boolean" + } + ], + "resultSchemaId": "treeseed.command.watch/v1", + "execution": { + "kind": "operation", + "operationId": "execution.graph.watch", + "input": [ + { + "target": "path", + "field": "teamId", + "source": "context", + "name": "team", + "required": true, + "transform": "identity" + }, + { + "target": "query", + "field": "cursor", + "source": "option", + "name": "cursor", + "required": false, + "transform": "identity" + }, + { + "target": "query", + "field": "waitSeconds", + "source": "option", + "name": "wait", + "required": false, + "transform": "integer" + } + ] + } + } + ] }, { - "nodeType": "leaf", - "segment": "watch", - "description": "Watch the selected resource.", - "kind": "read", - "arguments": [ + "nodeType": "branch", + "segment": "node", + "description": "Node operations.", + "children": [ { - "name": "assignment", - "description": "assignment identity or path.", - "required": true + "nodeType": "leaf", + "segment": "show", + "description": "Show the selected resource.", + "kind": "read", + "arguments": [ + { + "name": "node", + "description": "node identity or path.", + "required": true + } + ], + "resultSchemaId": "treeseed.command.show/v1", + "execution": { + "kind": "operation", + "operationId": "execution.nodes.show", + "input": [ + { + "target": "path", + "field": "teamId", + "source": "context", + "name": "team", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "nodeId", + "source": "argument", + "name": "node", + "required": true, + "transform": "identity" + } + ] + } + }, + { + "nodeType": "leaf", + "segment": "explain", + "description": "Explain the selected resource.", + "kind": "read", + "arguments": [ + { + "name": "node", + "description": "node identity or path.", + "required": true + } + ], + "resultSchemaId": "treeseed.command.explain/v1", + "execution": { + "kind": "operation", + "operationId": "execution.nodes.explain", + "input": [ + { + "target": "path", + "field": "teamId", + "source": "context", + "name": "team", + "required": true, + "transform": "identity" + }, + { + "target": "path", + "field": "nodeId", + "source": "argument", + "name": "node", + "required": true, + "transform": "identity" + } + ] + } } - ], - "resultSchemaId": "treeseed.command.watch/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." - } + ] }, { "nodeType": "leaf", - "segment": "retry", - "description": "Retry the selected resource.", + "segment": "reconcile", + "description": "Reconcile the selected resource.", "kind": "mutation", - "arguments": [ - { - "name": "assignment", - "description": "assignment identity or path.", - "required": true - } - ], "options": [ { "name": "--plan", "description": "Return the exact proposed outcome without mutation.", "type": "boolean" + }, + { + "name": "--project", + "description": "Reconcile one affected project component.", + "type": "string" } ], "authorization": { - "capability": "command.retry", - "confirmation": "authority" + "capability": "command.reconcile", + "confirmation": "never" }, - "resultSchemaId": "treeseed.command.retry/v1", + "resultSchemaId": "treeseed.command.reconcile/v1", "execution": { "kind": "operation", - "operationId": "assignments.retry", + "operationId": "execution.reconcile", "input": [ { "target": "path", @@ -5700,98 +6691,93 @@ "required": true, "transform": "identity" }, - { - "target": "path", - "field": "assignmentId", - "source": "argument", - "name": "assignment", - "required": true, - "transform": "identity" - }, { "target": "body", - "field": "reason", + "field": "projectId", "source": "option", - "name": "reason", + "name": "project", "required": false, "transform": "identity" - } - ] - } - }, - { - "nodeType": "leaf", - "segment": "cancel", - "description": "Cancel the selected resource.", - "kind": "mutation", - "arguments": [ - { - "name": "assignment", - "description": "assignment identity or path.", - "required": true - } - ], - "options": [ - { - "name": "--plan", - "description": "Return the exact proposed outcome without mutation.", - "type": "boolean" - } - ], - "authorization": { - "capability": "command.cancel", - "confirmation": "destructive" - }, - "resultSchemaId": "treeseed.command.cancel/v1", - "execution": { - "kind": "operation", - "operationId": "assignments.cancel", - "input": [ - { - "target": "path", - "field": "teamId", - "source": "context", - "name": "team", - "required": true, - "transform": "identity" - }, - { - "target": "path", - "field": "assignmentId", - "source": "argument", - "name": "assignment", - "required": true, - "transform": "identity" }, { "target": "body", - "field": "reason", + "field": "plan", "source": "option", - "name": "reason", + "name": "plan", "required": false, - "transform": "identity" + "transform": "boolean" } ] } }, { - "nodeType": "leaf", - "segment": "artifacts", - "description": "Artifacts the selected resource.", - "kind": "read", - "arguments": [ + "nodeType": "branch", + "segment": "assignments", + "description": "Assignments operations.", + "children": [ { - "name": "assignment", - "description": "assignment identity or path.", - "required": true + "nodeType": "leaf", + "segment": "list", + "description": "List the selected resource.", + "kind": "read", + "options": [ + { + "name": "--status", + "description": "Filter by assignment status.", + "type": "string" + }, + { + "name": "--limit", + "description": "Page size.", + "type": "number" + }, + { + "name": "--cursor", + "description": "Opaque page cursor.", + "type": "string" + } + ], + "resultSchemaId": "treeseed.command.list/v1", + "execution": { + "kind": "operation", + "operationId": "execution.assignments.list", + "input": [ + { + "target": "path", + "field": "teamId", + "source": "context", + "name": "team", + "required": true, + "transform": "identity" + }, + { + "target": "query", + "field": "status", + "source": "option", + "name": "status", + "required": false, + "transform": "identity" + }, + { + "target": "query", + "field": "limit", + "source": "option", + "name": "limit", + "required": false, + "transform": "integer" + }, + { + "target": "query", + "field": "cursor", + "source": "option", + "name": "cursor", + "required": false, + "transform": "identity" + } + ] + } } - ], - "resultSchemaId": "treeseed.command.artifacts/v1", - "execution": { - "kind": "unavailable", - "code": "standards_migration_not_enabled", - "reason": "This capability is not enabled until its control-plane operation is accepted." - } + ] } ] }, diff --git a/src/cli/commands/development-support/lifecycle.ts b/src/cli/commands/development-support/lifecycle.ts index dfe1f06..10f170e 100644 --- a/src/cli/commands/development-support/lifecycle.ts +++ b/src/cli/commands/development-support/lifecycle.ts @@ -41,8 +41,13 @@ export async function withDevelopmentLifecycle(env: NodeJS.ProcessEnv, action * Source changes enter through explicit restart/rebuild, not another use/resume. */ export function managedContainerAlreadyReady(value: unknown, sessionId: string, targetId: string): boolean { - const result = value as { registered?: unknown; state?: unknown; instances?: unknown; ready?: unknown }; + const result = value as { registered?: unknown; state?: unknown; instances?: unknown; ready?: unknown; digest?: unknown; activeDigest?: unknown }; if (result?.registered === false) return false; + if (result?.registered === true && targetId === 'sandbox' && typeof result.digest === 'string' && typeof result.activeDigest === 'string') { + if (!/^sha256:[a-f0-9]{64}$/u.test(result.digest) || !/^sha256:[a-f0-9]{64}$/u.test(result.activeDigest)) + throw new Error('Managed sandbox runtime identity is invalid.'); + return result.ready === true && result.digest === result.activeDigest; + } if (result?.registered === true && Array.isArray(result.instances)) { const instances = result.instances as Array<{ sessionId?: unknown; target?: unknown; running?: unknown; health?: unknown }>; if (!instances.length || instances.some((item) => item.sessionId !== sessionId || typeof item.target !== 'string' diff --git a/src/cli/commands/development-support/manager/invoke.ts b/src/cli/commands/development-support/manager/invoke.ts new file mode 100644 index 0000000..261c779 --- /dev/null +++ b/src/cli/commands/development-support/manager/invoke.ts @@ -0,0 +1,11 @@ +import type { CommandContext } from '../../../types.ts'; +import { invokeLocalHostManager } from '../../../support/host-client.js'; + +function hostCommand(handlerId: string, payload: unknown) { + return { handlerId, arguments: [], options: { payload: JSON.stringify(payload) } }; +} + +export async function invokeDevelopmentManager(context: CommandContext, handlerId: string, payload: unknown) { + const command = hostCommand(handlerId, payload); + return context.hostInvoke ? context.hostInvoke(command) : invokeLocalHostManager(command); +} diff --git a/src/cli/commands/development-support/overlays.ts b/src/cli/commands/development-support/overlays.ts index 7ec3387..4e5cfb9 100644 --- a/src/cli/commands/development-support/overlays.ts +++ b/src/cli/commands/development-support/overlays.ts @@ -61,31 +61,42 @@ export function installPackageOverlay(state: OverlaySessionState, record: { sess if (target.kind !== 'package-watch') return; const packageName = (JSON.parse(readFileSync(resolve(worktree, 'package.json'), 'utf8')) as { name?: string }).name; if (!packageName) throw new Error(`${runtime.project.id} package overlay has no package name.`); - const planned: Array<{ link: string; backup: string; owned: boolean }> = []; + const planned: Array<{ link: string; backup: string; owned: boolean; repair: boolean }> = []; for (const consumerId of affectedConsumers(record.runtimes, runtime.project.id, target.id)) { const consumer = record.session.repositories.find((entry) => entry.projectId === consumerId); if (!consumer) continue; const link = resolve(consumer.worktree, 'node_modules', ...packageName.split('/')); const backup = `${link}.treeseed-release-${state.sessionId}`; let owned = false; try { owned = lstatSync(link).isSymbolicLink() && resolve(dirname(link), readlinkSync(link)) === resolve(overlayRoot, 'current'); } catch { /* No existing link. */ } + const recorded = state.overlays.some(overlay => overlay.projectId === runtime.project.id && overlay.link === link + && resolve(overlay.overlayRoot) === resolve(overlayRoot)); + let repair = false; if (!owned) { try { - if (lstatSync(link).isSymbolicLink()) throw new Error(`Another development overlay blocks ${link}; stop or recover its owning session first.`); + if (lstatSync(link).isSymbolicLink()) { + if (recorded) repair = true; + else throw new Error(`Another development overlay blocks ${link}; stop or recover its owning session first.`); + } } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } } try { if (lstatSync(backup).isSymbolicLink()) throw new Error(`Development overlay backup is not a release directory: ${backup}.`); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } - if (existsSync(backup) && !owned) throw new Error(`Stale development overlay backup blocks ${link}.`); - planned.push({ link, backup, owned }); + if (existsSync(backup) && !owned && !repair) throw new Error(`Stale development overlay backup blocks ${link}.`); + planned.push({ link, backup, owned, repair }); } // Validate every ownership boundary before changing any consumer. A recovered // exact link is evidence of ownership; a similarly named backup alone is not. - for (const { link, backup, owned } of planned) { + for (const { link, backup, owned, repair } of planned) { if (owned) { if (!state.overlays.some(overlay => overlay.link === link)) state.overlays.push({ projectId: runtime.project.id, packageName, link, backup: existsSync(backup) ? backup : null, overlayRoot }); continue; } + if (repair) { + rmSync(link, { recursive: true, force: true }); + symlinkSync(relativeOverlayTarget(link, overlayRoot), link, 'dir'); + continue; + } mkdirSync(dirname(link), { recursive: true }); let retained: string | null = null; try { lstatSync(link); renameSync(link, backup); retained = backup; } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } diff --git a/src/cli/commands/development-support/selection.ts b/src/cli/commands/development-support/selection.ts new file mode 100644 index 0000000..6ac45e8 --- /dev/null +++ b/src/cli/commands/development-support/selection.ts @@ -0,0 +1,22 @@ +import type { DevelopmentRuntime, DevelopmentTarget } from '@treeseed/sdk/development'; + +export function parseDevelopmentSelection(value: string) { + const match = /^([a-z][a-z0-9.-]{1,63})\.([a-z][a-z0-9.-]{1,63})=(released|candidate|live)$/u.exec(value); + if (!match) throw new Error(`Invalid development selection ${value}; expected project.target=mode.`); + return { projectId: match[1]!, targetId: match[2]!, mode: match[3]! as 'released' | 'candidate' | 'live' }; +} + +export function selectedDevelopmentTarget(record: unknown, projectId: string, targetId: string) { + const runtime = (record as { runtimes?: DevelopmentRuntime[] }).runtimes?.find((entry) => entry.project.id === projectId); + const target = runtime?.targets.find((entry) => entry.id === targetId); + if (!runtime || !target) throw new Error(`Development target ${projectId}.${targetId} is not part of the current session.`); + return { runtime, target }; +} + +export function dependentDevelopmentAction(reaction: 'none' | 'restart' | 'rebuild' | 'manual', target: Pick) { + if (reaction === 'manual') return 'manual' as const; + if (reaction !== 'rebuild') return 'restart' as const; + if (target.kind === 'package-watch') return 'package-rebuild' as const; + if (target.kind === 'rebuild-restart') return 'rebuild-restart' as const; + return target.operations.build ? 'build-only' as const : 'restart' as const; +} diff --git a/src/cli/commands/development.ts b/src/cli/commands/development.ts index 58f4320..f620e95 100644 --- a/src/cli/commands/development.ts +++ b/src/cli/commands/development.ts @@ -5,7 +5,6 @@ import { basename, dirname, isAbsolute, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { developmentCandidateSchema, type DevelopmentRuntime, type DevelopmentTarget } from '@treeseed/sdk/development'; import type { CommandContext, ParsedInvocation } from '../types.js'; -import { invokeLocalHostManager } from '../support/host-client.js'; import { developmentStateRoot, selectDevelopmentCli } from './development-cli-selection.js'; import { dependentReactions, installPackageOverlay, overlayGeneration, relativeOverlayTarget, restoreOverlays, startPackageSynchronizer, stopProcess, stopProcesses, waitForNewPackageOverlay, waitForPackageOverlay } from './development-support/overlays.js'; import { artifactPaths, compatibilityAttestations, withFreezeLock } from './development-support/candidate.js'; @@ -15,6 +14,8 @@ import { ownsDevelopmentProcess, processIdentity } from './development-support/p import { developmentBootOrder } from './development-support/boot-order.js'; import { assertNoSelectedDevelopmentCustody, managedContainerAlreadyReady, withDevelopmentLifecycle } from './development-support/lifecycle.js'; import { loadDevelopmentRuntimes } from './development-support/runtime-loader.js'; +import { dependentDevelopmentAction, parseDevelopmentSelection as parseSelection, selectedDevelopmentTarget as selectedTarget } from './development-support/selection.js'; +import { invokeDevelopmentManager as invoke } from './development-support/manager/invoke.js'; export { relativeOverlayTarget, startPackageSynchronizer, stopProcess, waitForNewPackageOverlay } from './development-support/overlays.js'; export { developmentCliEntrypointPath, selectDevelopmentCli } from './development-cli-selection.js'; @@ -58,7 +59,6 @@ function loadState(env: NodeJS.ProcessEnv, sessionId?: unknown) { function sha256(value: string | Buffer) { return `sha256:${createHash('sha256').update(value).digest('hex')}`; } function sha512Integrity(value: Buffer) { return `sha512-${createHash('sha512').update(value).digest('base64')}`; } function git(root: string, args: string[]) { return execFileSync('git', ['-C', root, ...args], { encoding: 'utf8' }).trim(); } - function repositoryClosure(runtime: DevelopmentRuntime, worktree: string, excludedPaths: string[] = []) { const pathspec = excludedPaths.length ? ['--', '.', ...excludedPaths.map((path) => `:(exclude)${path}`)] : []; const status = git(worktree, ['status', '--porcelain=v1', '--untracked-files=all', ...pathspec]); @@ -66,27 +66,6 @@ function repositoryClosure(runtime: DevelopmentRuntime, worktree: string, exclud return { projectId: runtime.project.id, repository: runtime.project.repository, worktree, commit: git(worktree, ['rev-parse', 'HEAD']), branch, dirty: Boolean(status), dirtyDigest: status ? sha256(`${status}\n${git(worktree, ['diff', '--binary', 'HEAD', ...pathspec])}`) : null, recipeDigest: sha256(JSON.stringify(runtime)) }; } - -function hostCommand(handlerId: string, payload: unknown) { - return { handlerId, arguments: [], options: { payload: JSON.stringify(payload) } }; -} -async function invoke(context: CommandContext, handlerId: string, payload: unknown) { - const command = hostCommand(handlerId, payload); - return context.hostInvoke ? context.hostInvoke(command) : invokeLocalHostManager(command); -} -function parseSelection(value: string) { - const match = /^([a-z][a-z0-9.-]{1,63})\.([a-z][a-z0-9.-]{1,63})=(released|candidate|live)$/u.exec(value); - if (!match) throw new Error(`Invalid development selection ${value}; expected project.target=mode.`); - return { projectId: match[1]!, targetId: match[2]!, mode: match[3]! as 'released' | 'candidate' | 'live' }; -} -function selectedTarget(record: unknown, projectId: string, targetId: string) { - const value = record as { runtimes?: DevelopmentRuntime[] }; - const runtime = value.runtimes?.find((entry) => entry.project.id === projectId); - const target = runtime?.targets.find((entry) => entry.id === targetId); - if (!runtime || !target) throw new Error(`Development target ${projectId}.${targetId} is not part of the current session.`); - return { runtime, target }; -} - function operationForMode(target: DevelopmentTarget, mode: string) { if (mode === 'released') return null; if (String(target.kind) === 'source-check') return target.operations.verify ?? null; @@ -103,7 +82,6 @@ export function usesManagedContainer(target: DevelopmentTarget) { function usesManagerBuild(target: DevelopmentTarget) { return (target as DevelopmentTarget & { executionCustody?: string }).executionCustody === 'manager'; } - async function containerOperation(context: CommandContext, sessionId: string, runtime: DevelopmentRuntime, target: DevelopmentTarget, action: 'start' | 'stop' | 'status' | 'logs') { return invoke(context, 'local.dev.container', {sessionId,projectId:runtime.project.id,targetId:target.id,action}); } @@ -111,7 +89,6 @@ async function containerOperation(context: CommandContext, sessionId: string, ru export function developmentOperationEnvironment(state: Pick, worktree: string, mode: string, env: NodeJS.ProcessEnv, resolvedEnvironment: NodeJS.ProcessEnv = {}, operationEnvironment: NodeJS.ProcessEnv = {}) { return { ...env, ...resolvedEnvironment, TREESEED_DEVELOPMENT_SESSION_ID: state.sessionId, TREESEED_DEVELOPMENT_MODE: mode, TREESEED_DEVELOPMENT_WORKSPACE_ROOT: state.workspaceRoot ?? dirname(state.manifest), TREESEED_DEVELOPMENT_WORKTREE: worktree, ...operationEnvironment }; } - function runOneShotOperation(state: LocalSessionState, operation: NonNullable, worktree: string, mode: string, env: NodeJS.ProcessEnv, resolvedEnvironment: NodeJS.ProcessEnv = {}) { const root = operation.cwd ? resolve(worktree, operation.cwd) : worktree; const result = spawnSync(operation.command, operation.args, { cwd: root, env: developmentOperationEnvironment(state, worktree, mode, env, resolvedEnvironment, operation.environment), stdio: 'pipe', timeout: operation.timeoutSeconds * 1_000 }); @@ -181,10 +158,12 @@ async function startSession(invocation: ParsedInvocation, context: CommandContex async function useTargets(invocation: Pick, context: CommandContext) { const state = loadState(context.env,invocation.options.session), sessionId = String(invocation.options.session ?? state.sessionId); - const record = await invoke(context, 'local.dev.status', { sessionId, all: false }) as { session: { status?: string; repositories: Array<{ projectId: string; worktree: string }> }; runtimes: DevelopmentRuntime[] }; - if (record.session.status === 'stopped') throw new Error('The development session has been explicitly stopped.'); const selections = [invocation.arguments[0]!, ...(Array.isArray(invocation.options.target) ? invocation.options.target : [])].map(parseSelection); if (invocation.options.plan === true) return { sessionId, selections, mutation: false }; + const runtimes = (await loadDevelopmentRuntimes(state.manifest)).map(({ runtime }) => runtime); + await invoke(context, 'local.dev.session.refresh', { sessionId, runtimes }); + const record = await invoke(context, 'local.dev.status', { sessionId, all: false }) as { session: { status?: string; repositories: Array<{ projectId: string; worktree: string }> }; runtimes: DevelopmentRuntime[] }; + if (record.session.status === 'stopped') throw new Error('The development session has been explicitly stopped.'); for (const selection of selections) { const { runtime, target } = selectedTarget(record, selection.projectId, selection.targetId); const repository = (record as { session: { repositories: Array<{ projectId: string; worktree: string }> } }).session.repositories.find((entry) => entry.projectId === selection.projectId); @@ -298,13 +277,17 @@ async function markRebuilt(context: CommandContext, sessionId: string, projectId await invoke(context, 'local.dev.use', { sessionId, projectId, targetId, mode, ...(!usesManagerBuild(target) && target.endpoints[0] ? { port: target.endpoints[0].port } : {}) }); } -async function rebuildPackage(input: { state: LocalSessionState; runtime: DevelopmentRuntime; target: DevelopmentTarget; worktree: string; mode: 'candidate' | 'live'; context: CommandContext }) { - const { state, runtime, target, worktree, mode, context } = input; +async function rebuildPackage(input: { state: LocalSessionState; record: { session: { repositories: Array<{ projectId: string; worktree: string }> }; runtimes: DevelopmentRuntime[] }; runtime: DevelopmentRuntime; target: DevelopmentTarget; worktree: string; mode: 'candidate' | 'live'; context: CommandContext }) { + const { state, record, runtime, target, worktree, mode, context } = input; if (!target.operations.build) throw new Error(`${runtime.project.id}.${target.id} does not declare a rebuild operation.`); const overlayRoot = resolve(worktree, '.treeseed', 'cache', 'development-sessions', state.sessionId, target.id); const previous = overlayGeneration(overlayRoot); runOneShotOperation(state, target.operations.build, worktree, mode, context.env); await waitForNewPackageOverlay(target, worktree, overlayRoot, previous); + // Recovery can retain a healthy synchronizer while a consumer link has been + // restored to its released package. Every rebuild reasserts the selected + // package overlay before restarting dependants. + installPackageOverlay(state, record, runtime, target, worktree, overlayRoot); await markRebuilt(context, state.sessionId, runtime.project.id, target.id, mode, target); } @@ -332,7 +315,8 @@ async function restartConsumer(input: { state: LocalSessionState; runtime: Devel async function restart(invocation: ParsedInvocation, context: CommandContext, state: LocalSessionState, sessionId: string) { const selection = parseSelection(`${invocation.arguments[0]}=candidate`); - const record = await invoke(context, 'local.dev.status', { sessionId, all: false }) as DevelopmentStatusRecord; + const status = await invoke(context, 'local.dev.status', { sessionId, all: false }) as DevelopmentStatusRecord; + const record = { ...status, runtimes: (await loadDevelopmentRuntimes(state.manifest)).map(({ runtime }) => runtime) }; const selected = record.session.targets.find((entry) => entry.projectId === selection.projectId && entry.targetId === selection.targetId); if (!selected || selected.mode === 'released') throw new Error(`${selection.projectId}.${selection.targetId} is not selected for local development.`); const { runtime, target } = selectedTarget(record, selection.projectId, selection.targetId); @@ -354,7 +338,10 @@ async function restart(invocation: ParsedInvocation, context: CommandContext, st async function rebuild(invocation: ParsedInvocation, context: CommandContext, state: LocalSessionState, sessionId: string) { const selection = parseSelection(`${invocation.arguments[0]}=candidate`); - const record = await invoke(context, 'local.dev.status', { sessionId, all: false }) as DevelopmentStatusRecord; + const runtimes = (await loadDevelopmentRuntimes(state.manifest)).map(({ runtime }) => runtime); + await invoke(context, 'local.dev.session.refresh', { sessionId, runtimes }); + const status = await invoke(context, 'local.dev.status', { sessionId, all: false }) as DevelopmentStatusRecord; + const record = { ...status, runtimes }; const selected = record.session.targets.find((entry) => entry.projectId === selection.projectId && entry.targetId === selection.targetId); if (!selected || selected.mode === 'released') throw new Error(`${selection.projectId}.${selection.targetId} is not selected for local development.`); const { runtime, target } = selectedTarget(record, selection.projectId, selection.targetId); @@ -371,7 +358,7 @@ async function rebuild(invocation: ParsedInvocation, context: CommandContext, st if (!target.operations.verify) throw new Error(`${selection.projectId}.${selection.targetId} does not declare verification.`); runOneShotOperation(state, target.operations.verify, repository.worktree, mode, context.env); await markRebuilt(context, sessionId, runtime.project.id, target.id, mode, target); - } else if (target.kind === 'package-watch') await rebuildPackage({ state, runtime, target, worktree: repository.worktree, mode, context }); + } else if (target.kind === 'package-watch') await rebuildPackage({ state, record, runtime, target, worktree: repository.worktree, mode, context }); else if (target.kind === 'rebuild-restart') { if (usesManagerBuild(target)) { await restartConsumer({ state, runtime, target, worktree: repository.worktree, mode, context }); @@ -391,10 +378,12 @@ async function rebuild(invocation: ParsedInvocation, context: CommandContext, st const dependentSelection = record.session.targets.find((entry) => entry.projectId === dependent.runtime.project.id && entry.targetId === dependent.target.id); const dependentRepository = record.session.repositories.find((entry) => entry.projectId === dependent.runtime.project.id); if (!dependentSelection || dependentSelection.mode === 'released' || !dependentRepository) continue; - if (dependent.reaction === 'manual') { manual.push(`${dependent.runtime.project.id}.${dependent.target.id}`); continue; } - const dependentInput = { state, runtime: dependent.runtime, target: dependent.target, worktree: dependentRepository.worktree, mode: dependentSelection.mode as 'candidate' | 'live', context }; - if (dependent.reaction === 'rebuild' && dependent.target.kind === 'package-watch') await rebuildPackage(dependentInput); - else if (dependent.reaction === 'rebuild' && dependent.target.operations.build) { + const action = dependentDevelopmentAction(dependent.reaction, dependent.target); + if (action === 'manual') { manual.push(`${dependent.runtime.project.id}.${dependent.target.id}`); continue; } + const dependentInput = { state, record, runtime: dependent.runtime, target: dependent.target, worktree: dependentRepository.worktree, mode: dependentSelection.mode as 'candidate' | 'live', context }; + if (action === 'package-rebuild') await rebuildPackage(dependentInput); + else if (action === 'rebuild-restart') await restartConsumer(dependentInput); + else if (action === 'build-only') { runOneShotOperation(state, dependent.target.operations.build, dependentRepository.worktree, dependentSelection.mode, context.env); await markRebuilt(context, sessionId, dependent.runtime.project.id, dependent.target.id, dependentSelection.mode, dependent.target); } else await restartConsumer(dependentInput); @@ -492,6 +481,17 @@ async function runDevelopmentUnlocked(invocation: ParsedInvocation, context: Com if (invocation.command.name === 'dev rebuild') { return rebuild(invocation, context, state, sessionId); } + if (invocation.command.name === 'dev migrate') { + const selection = parseSelection(`${String(invocation.arguments[0] ?? '')}=candidate`); + if (selection.projectId !== 'api' || selection.targetId !== 'service') throw new Error('The first development migration target is api.service.'); + if (invocation.options.plan === true) return { action: 'migrate', sessionId, target: 'api.service', mutation: false }; + const record = await invoke(context, 'local.dev.status', { sessionId, all: false }) as DevelopmentStatusRecord; + const { target } = selectedTarget(record, selection.projectId, selection.targetId); + const repository = record.session.repositories.find((entry) => entry.projectId === selection.projectId); + if (!repository || !target.operations.build) throw new Error('API development build operation is unavailable.'); + runOneShotOperation(state, target.operations.build, repository.worktree, 'candidate', context.env); + return invoke(context, 'local.dev.migrate', { sessionId, projectId: 'api', targetId: 'service' }); + } if (invocation.command.name === 'dev restart') return restart(invocation, context, state, sessionId); if (invocation.command.name === 'dev freeze') return freeze(invocation, context); if (invocation.command.name === 'dev verify') return verifyCandidate(invocation, context); diff --git a/src/cli/commands/operator.ts b/src/cli/commands/operator.ts index 02fe910..08d15dd 100644 --- a/src/cli/commands/operator.ts +++ b/src/cli/commands/operator.ts @@ -11,6 +11,7 @@ import { controlPlaneServerRegistry, createControlPlaneClient } from '../support import { loadServerSession } from '../support/server-custody.js'; import { renderCommunicationResponses } from '../support/human-renderer.js'; import { resolveExplicitTeam } from '../support/selectors/team.js'; +import { resolveExplicitProject } from '../support/selectors/project.js'; import { getOperationInputField, setOperationInputField } from '../support/operations/input-fields.js'; function activeTeam(invocation: ParsedInvocation, context: CommandContext) { @@ -75,6 +76,13 @@ async function operationInput(invocation: ParsedInvocation, context: CommandCont if (diagnostics.length) throw Object.assign(new Error(diagnostics.map(item => `${item.path}: ${item.message}`).join(' ')), { category: 'invalid_input', code: 'workday_agent_selection_invalid' }); input.body.agentSelection = normalizeWorkdayAgentSelection(input.body.agentSelection); } + if (operation.descriptor.operationId === 'workdays.plan' && input.body.decisionIds !== undefined) { + const values = Array.isArray(input.body.decisionIds) ? input.body.decisionIds : []; + if (!values.length || values.length > 64 || values.some(value => typeof value !== 'string' || !value.trim() || value.length > 128)) { + throw Object.assign(new Error('decisionIds must contain one to 64 non-empty decision identities.'), { category: 'invalid_input', code: 'workday_decision_selection_invalid' }); + } + input.body.decisionIds = [...new Set(values.map(value => String(value).trim()))].sort(); + } if (operation.descriptor.operationId.startsWith('seeds.') && typeof input.body.file === 'string') { const parsed = await portableSeedBundle(input.body.file, context); delete input.body.file; @@ -120,6 +128,17 @@ export async function runOperator(invocation: ParsedInvocation, context: Command for (const slot of slots) slot.teamId = teamId; } } + if (typeof invocation.options.project === 'string') { + const slots = [input.path, input.query, input.body].filter((slot): slot is Record => Boolean(slot) && slot?.projectId === invocation.options.project); + if (slots.length) { + const teamId = typeof input.path.teamId === 'string' ? input.path.teamId + : typeof input.query.teamId === 'string' ? input.query.teamId + : typeof input.body?.teamId === 'string' ? input.body.teamId + : activeTeam(invocation, context); + const projectId = await resolveExplicitProject(invocation, context, invocation.options.project, teamId); + for (const slot of slots) slot.projectId = projectId; + } + } if (operation.descriptor.operationId === 'communications.send' && !input.body?.message) { if (!context.interactiveUi || !process.stdin.isTTY || !process.stdout.isTTY || invocation.options.json || invocation.options.jsonStream) return runInteractiveChat(invocation, context, String(input.path.teamId), typeof input.path.channel === 'string' ? input.path.channel : undefined); return launchApplication(context, { server: typeof invocation.options.server === 'string' ? invocation.options.server : undefined, workspace: 'chat' }); @@ -134,7 +153,11 @@ export async function runOperator(invocation: ParsedInvocation, context: Command new Error(`Deprecated --to target ${target} is not addressed in the message.`), { category: 'invalid_input', code: 'communication_to_not_mentioned' }); if (input.body) input.body.recipients = compatibility.length ? compatibility : undefined; } - if (invocation.options.plan === true) return { operationId: operation.descriptor.operationId, input, mutation: false }; + // Reconciliation planning is itself an API-owned read-only computation over + // current TreeDX and graph state. Invoke that explicit plan contract instead + // of returning the generic client-side mutation preview. + const serverSidePlan = invocation.options.plan === true && operation.descriptor.operationId === 'execution.reconcile'; + if (invocation.options.plan === true && !serverSidePlan) return { operationId: operation.descriptor.operationId, input, mutation: false }; if (context.operationInvoke) return context.operationInvoke(operation.descriptor.operationId, input); const { client, profile } = await createControlPlaneClient(invocation, context, operation.descriptor.authentication !== 'anonymous'); const options = operation.descriptor.kind === 'mutation' ? { idempotencyKey: String(invocation.options.idempotencyKey ?? randomUUID()), headers: {} as Record } : { headers: {} as Record }; diff --git a/src/cli/registry.ts b/src/cli/registry.ts index 123196a..a8363f6 100644 --- a/src/cli/registry.ts +++ b/src/cli/registry.ts @@ -48,8 +48,10 @@ function options(path: string[], leaf: CommandLeafDescriptor) { if (operation.kind === 'mutation') selected.push({ name: 'idempotencyKey', flag: '--idempotency-key', kind: 'string', description: 'Reuse the same request identity when retrying this mutation.' }); } for (const option of leaf.options ?? []) { - if (selected.some((candidate) => candidate.flag === option.name)) continue; - selected.push({ name: option.name.slice(2).replace(/-([a-z])/gu, (_, letter: string) => letter.toUpperCase()), flag: option.name, kind: option.type, description: option.description }); + const descriptor = { name: option.name.slice(2).replace(/-([a-z])/gu, (_, letter: string) => letter.toUpperCase()), flag: option.name, kind: option.type, description: option.description } as OptionSpec; + const existing = selected.findIndex((candidate) => candidate.flag === option.name); + if (existing >= 0) selected.splice(existing, 1, descriptor); + else selected.push(descriptor); } return selected; } diff --git a/src/cli/support/selectors/project.ts b/src/cli/support/selectors/project.ts new file mode 100644 index 0000000..2aa486c --- /dev/null +++ b/src/cli/support/selectors/project.ts @@ -0,0 +1,54 @@ +import { CONTROL_PLANE_OPERATIONS } from '@treeseed/sdk/operator-contracts'; +import type { CommandContext, ParsedInvocation } from '../../types.js'; +import { createControlPlaneClient } from '../client.js'; + +type Project = { id: string; slug: string; teamId?: string; team_id?: string }; +type Page = { items?: Project[]; projects?: Project[]; page?: { hasMore?: boolean; nextCursor?: string | null } }; +const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu; + +export async function resolveProjectSelector( + selector: string, + teamId: string | undefined, + read: (cursor?: string) => Promise, +): Promise { + if (uuid.test(selector)) return selector; + const matches = new Set(); + const cursors = new Set(); + let cursor: string | undefined; + for (let page = 0; page < 100; page += 1) { + const result = await read(cursor); + for (const project of result.items ?? result.projects ?? []) { + const projectTeamId = project.teamId ?? project.team_id; + if ((!teamId || !projectTeamId || projectTeamId === teamId) + && (project.id === selector || project.slug.toLowerCase() === selector.toLowerCase())) matches.add(project.id); + } + if (!result.page?.hasMore) { + if (matches.size !== 1) throw Object.assign( + new Error(matches.size ? 'Project selector is ambiguous.' : 'Project is not accessible.'), + { category: matches.size ? 'ambiguous_context' : 'not_found', code: matches.size ? 'project_ambiguous' : 'project_not_found' }, + ); + return [...matches][0]!; + } + const next = result.page.nextCursor; + if (!next || cursors.has(next)) throw new Error('Project inventory pagination is incomplete.'); + cursors.add(next); + cursor = next; + } + throw new Error('Project inventory exceeds the bounded lookup limit.'); +} + +export async function resolveExplicitProject( + invocation: ParsedInvocation, + context: CommandContext, + selector: string, + teamId?: string, +) { + return resolveProjectSelector(selector, teamId, async cursor => { + const input = { path: {}, query: { limit: 200, ...(teamId ? { teamId } : {}), ...(cursor ? { cursor } : {}) }, body: undefined }; + const response = context.operationInvoke + ? await context.operationInvoke('projects.list', input) + : await (await createControlPlaneClient(invocation, context, true)).client.invoke(CONTROL_PLANE_OPERATIONS.projects.list, input); + const value = response as { data?: Page } & Page; + return value.data ?? value; + }); +} diff --git a/tests/unit/command-boundary/canonical-client.test.ts b/tests/unit/command-boundary/canonical-client.test.ts index 98e8db1..c178ee2 100644 --- a/tests/unit/command-boundary/canonical-client.test.ts +++ b/tests/unit/command-boundary/canonical-client.test.ts @@ -44,7 +44,8 @@ test('teams use persists the active team and team commands inherit it', async () test('leaf commands expose only catalog-derived high-level options', () => { const byName = new Map(commandSpecs.map((command) => [command.name, command.options.map((option) => option.flag)])); assert.deepEqual(byName.get('workdays start'), ['--server', '--team', '--preflight', '--digest', '--yes', '--json', '--idempotency-key', '--plan']); - assert.deepEqual(byName.get('plans show'), ['--server', '--json']); + assert.equal(byName.has('plans show'), false); + assert.deepEqual(byName.get('workdays stop'), ['--server', '--team', '--reason', '--yes', '--json', '--idempotency-key', '--plan']); assert.deepEqual(byName.get('agents show'), ['--server', '--project', '--json']); assert.deepEqual(byName.get('host status'), ['--server', '--json']); assert.deepEqual(byName.get('host provider environment set'), ['--server', '--yes', '--json', '--plan', '--stdin']); @@ -176,6 +177,18 @@ test('plan mode prevents mutation operation invocation', async () => { assert.equal(JSON.parse(output[0]!).result.mutation, false); }); +test('execution reconciliation plan invokes the API-owned read-only diff', async () => { + const output: string[] = []; const invocations: Array<{ operationId: string; input: any }> = []; + const projectId = '22222222-2222-4222-8222-222222222222'; + const exit = await runCommandLine(['execution', 'reconcile', '--team', '11111111-1111-4111-8111-111111111111', '--project', projectId, '--plan', '--json'], { + interactiveUi: false, operationInvoke: async (operationId, input) => { invocations.push({ operationId, input }); return { data: { changes: [] } }; }, + write: (value) => output.push(value), + }); + assert.equal(exit, 0); + assert.deepEqual(invocations, [{ operationId: 'execution.reconcile', input: { path: { teamId: '11111111-1111-4111-8111-111111111111' }, query: {}, body: { projectId, plan: true } } }]); + assert.deepEqual(JSON.parse(output[0]!).result, { changes: [] }); +}); + test('plan mode is non-mutating for local credential custody', async () => { const output: string[] = []; const exit = await runCommandLine(['auth', 'logout', '--plan', '--server', 'local', '--json'], { interactiveUi: false, write: (value) => output.push(value) }); diff --git a/tests/unit/command-boundary/development/dependent-rebuild.test.ts b/tests/unit/command-boundary/development/dependent-rebuild.test.ts new file mode 100644 index 0000000..c254a5c --- /dev/null +++ b/tests/unit/command-boundary/development/dependent-rebuild.test.ts @@ -0,0 +1,14 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { dependentDevelopmentAction } from '../../../../src/cli/commands/development-support/selection.ts'; + +test('source-coupled service consumers rebuild and restart', () => { + const target = { kind: 'rebuild-restart', operations: { build: { command: 'npm', args: ['run', 'build'] } } } as const; + assert.equal(dependentDevelopmentAction('rebuild', target as never), 'rebuild-restart'); +}); + +test('dependency reactions preserve package and manual boundaries', () => { + assert.equal(dependentDevelopmentAction('rebuild', { kind: 'package-watch', operations: { build: {} } } as never), 'package-rebuild'); + assert.equal(dependentDevelopmentAction('manual', { kind: 'rebuild-restart', operations: {} } as never), 'manual'); + assert.equal(dependentDevelopmentAction('restart', { kind: 'rebuild-restart', operations: { build: {} } } as never), 'restart'); +}); diff --git a/tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts b/tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts index 600a5b1..672f5f3 100644 --- a/tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts +++ b/tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts @@ -11,7 +11,18 @@ test('boot resume and manual use re-read state under the same lifecycle lock', { const sessionId = 'dev-test', env = { ...process.env, XDG_STATE_HOME: root }; const directory = resolve(root, 'treeseed/development'); mkdirSync(directory, { recursive: true, mode: 0o700 }); - writeFileSync(resolve(directory, 'current.json'), JSON.stringify({ sessionId, manifest: resolve(root, 'session.yaml'), processes: {}, overlays: [], candidates: [] })); + const manifest = resolve(root, 'session.yaml'); + writeFileSync(manifest, `projects:\n - manifest: api/treeseed.package.yaml\n worktree: .\n`); + mkdirSync(resolve(root, 'api'), { recursive: true, mode: 0o700 }); + writeFileSync(resolve(root, 'api/treeseed.package.yaml'), JSON.stringify({ development: { + schemaVersion: 'treeseed.development-runtime/v2', project: { id: 'api', repository: 'treeseed-ai/api' }, defaults: { restoreOnFailure: true }, + targets: [{ id: 'operations-runner', kind: 'rebuild-restart', executionCustody: 'manager', platforms: ['linux-amd64'], runtimeRequirements: [], + sourceRoots: ['src'], ignoredPaths: [], operations: { start: { command: 'manager-runtime' } }, ready: { kind: 'process', graceSeconds: 0 }, + outputs: [], endpoints: [{ id: 'http', protocol: 'http', port: 4000, visibility: 'loopback', authentication: 'application' }], dependencies: [], + statePolicy: 'stateless', migrationPolicy: 'none', secretRefs: {}, shutdown: { graceSeconds: 1, activeWorkPolicy: 'block' }, resources: {}, logs: [], + forbiddenOperations: [], promotion: { liveAdmissible: false, candidateRequiresVerification: true } }], + } })); + writeFileSync(resolve(directory, 'current.json'), JSON.stringify({ sessionId, manifest, processes: {}, overlays: [], candidates: [] })); const record = { session: { sessionId, status: 'active', repositories: [{ projectId: 'api', worktree: root }], targets: [{ projectId: 'api', targetId: 'operations-runner', mode: 'candidate', generation: 0, health: 'pending' }] }, @@ -24,6 +35,7 @@ test('boot resume and manual use re-read state under the same lifecycle lock', { hostInvoke: async (request: { handlerId: string; options: { payload?: unknown } }) => { const payload = JSON.parse(String(request.options.payload)); if (request.handlerId === 'local.dev.status') return record; + if (request.handlerId === 'local.dev.session.refresh') return record; if (request.handlerId === 'local.dev.use') { assert.equal(payload.port, undefined, 'Manager-custody targets must not receive redundant host-port readiness probes'); return record; diff --git a/tests/unit/command-boundary/development/lifecycle.test.ts b/tests/unit/command-boundary/development/lifecycle.test.ts index 5e99b74..3e93c92 100644 --- a/tests/unit/command-boundary/development/lifecycle.test.ts +++ b/tests/unit/command-boundary/development/lifecycle.test.ts @@ -85,6 +85,14 @@ test('healthy multi-container manager runtime is reusable', () => { assert.equal(managedContainerAlreadyReady({ registered: true, instances: [{ ...instances[0], running: false }], ready: false }, 'dev-test', 'provider'), false); }); +test('managed Agent sandbox status recognizes exact active guest-image custody', () => { + const digest = `sha256:${'a'.repeat(64)}`; + assert.equal(managedContainerAlreadyReady({ registered: false, state: null }, 'dev-test', 'sandbox'), false); + assert.equal(managedContainerAlreadyReady({ registered: true, ready: true, digest, activeDigest: digest }, 'dev-test', 'sandbox'), true); + assert.equal(managedContainerAlreadyReady({ registered: true, ready: false, digest, activeDigest: `sha256:${'b'.repeat(64)}` }, 'dev-test', 'sandbox'), false); + assert.throws(() => managedContainerAlreadyReady({ registered: true, ready: true, digest: 'latest', activeDigest: digest }, 'dev-test', 'sandbox'), /identity/); +}); + test('registered unhealthy, malformed, or wrong-instance state never authorizes overwrite', () => { const value = { Name: 'treeseed-dev-test-api-operations-runner', State: 'running', Health: 'unhealthy' }; assert.equal(managedContainerAlreadyReady({ registered: true, state: JSON.stringify(value) }, 'dev-test', 'operations-runner'), false); diff --git a/tests/unit/command-boundary/recovery/overlay-readoption.test.ts b/tests/unit/command-boundary/recovery/overlay-readoption.test.ts index e557410..513084d 100644 --- a/tests/unit/command-boundary/recovery/overlay-readoption.test.ts +++ b/tests/unit/command-boundary/recovery/overlay-readoption.test.ts @@ -50,6 +50,17 @@ test('foreign development links and symlinked backups are rejected before mutati } finally { rmSync(f.root, { recursive: true, force: true }); } }); +test('a recorded session overlay repairs a package-manager link without replacing its release backup', () => { + const f = fixture(); try { + f.install(); const item = f.state.overlays[0]!; const originalBackup = item.backup; + rmSync(item.link, { recursive: true, force: true }); symlinkSync(resolve(f.root, 'package-manager/source'), item.link); + f.install(); + assert.equal(resolve(item.link, '..', readlinkSync(item.link)), resolve(f.overlay, 'current')); + assert.equal(f.state.overlays.length, 2); + assert.equal(f.state.overlays[0]!.backup, originalBackup); + } finally { rmSync(f.root, { recursive: true, force: true }); } +}); + test('restore refuses a symlinked backup without removing the active overlay', () => { const f = fixture(); try { f.install(); const item = f.state.overlays[0]!; rmSync(item.backup!, { recursive: true }); symlinkSync(f.overlay, item.backup!); diff --git a/tests/unit/command-boundary/selectors/project.test.ts b/tests/unit/command-boundary/selectors/project.test.ts new file mode 100644 index 0000000..5ba8c70 --- /dev/null +++ b/tests/unit/command-boundary/selectors/project.test.ts @@ -0,0 +1,21 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { resolveProjectSelector } from '../../../../src/cli/support/selectors/project.ts'; + +test('project selectors preserve exact IDs and resolve a team-scoped slug', async () => { + const id = '8cbfb810-6da5-4da2-9ae9-cad53101253f'; + assert.equal(await resolveProjectSelector(id, undefined, async () => { throw new Error('unexpected read'); }), id); + assert.equal(await resolveProjectSelector('sdk', 'team-a', async () => ({ + items: [ + { id, slug: 'sdk', teamId: 'team-a' }, + { id: 'other', slug: 'sdk', teamId: 'team-b' }, + ], + })), id); +}); + +test('project selectors reject missing and ambiguous slugs', async () => { + await assert.rejects(resolveProjectSelector('missing', undefined, async () => ({ items: [] })), { code: 'project_not_found' }); + await assert.rejects(resolveProjectSelector('sdk', undefined, async () => ({ + items: [{ id: 'one', slug: 'sdk' }, { id: 'two', slug: 'sdk' }], + })), { code: 'project_ambiguous' }); +}); diff --git a/tests/unit/command-boundary/workdays/selection.test.ts b/tests/unit/command-boundary/workdays/selection.test.ts index 0cf59c0..6abdee2 100644 --- a/tests/unit/command-boundary/workdays/selection.test.ts +++ b/tests/unit/command-boundary/workdays/selection.test.ts @@ -15,6 +15,15 @@ test('repeated and CSV selectors become a normalized intersecting nested intent' assert.equal(Object.keys(calls[0]!.input.body).some(key => key.includes('.')), false); }); +test('repeated and CSV accepted decisions become one normalized selection', async () => { + let body: any; + const exit = await runCommandLine([...base, '--decision', 'decision-b,decision-a', '--decision', 'decision-b'], { + interactiveUi: false, write() {}, operationInvoke: async (_operationId, input) => { body = input.body; return { data: {} }; }, + }); + assert.equal(exit, 0); + assert.deepEqual(body.decisionIds, ['decision-a', 'decision-b']); +}); + test('omitted selection leaves the full intent unchanged', async () => { let body: any; assert.equal(await runCommandLine(base, { interactiveUi: false, write() {}, operationInvoke: async (_id, input) => { body = input.body; return { data: {} }; } }), 0); @@ -32,6 +41,12 @@ for (const selector of [['--agent', ''], ['--agent', 'reviewer,'], ['--activity' }); } +test('empty decision selection never invokes the API', async () => { + let calls = 0; + assert.equal(await runCommandLine([...base, '--decision', 'decision,'], { interactiveUi: false, write() {}, operationInvoke: async () => { calls++; } }), 1); + assert.equal(calls, 0); +}); + test('nested bindings reject prototype traversal, collisions, and excessive depth', () => { const input = {}; setOperationInputField(input, 'agentSelection.agentSlugs', ['reviewer']); assert.deepEqual(getOperationInputField(input, 'agentSelection.agentSlugs'), ['reviewer']);