@b00dea/cockpit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +3 -0
  3. package/cockpit.schema.json +73 -3
  4. package/dist/autonomy.d.ts +3 -2
  5. package/dist/autonomy.js +30 -13
  6. package/dist/browser/commands.d.ts +57 -2
  7. package/dist/browser/commands.js +114 -10
  8. package/dist/browser/runtime.d.ts +1 -0
  9. package/dist/browser/runtime.js +4 -1
  10. package/dist/browser/surfaces.d.ts +7 -7
  11. package/dist/browser/surfaces.js +5 -0
  12. package/dist/canonical-json.d.ts +2 -0
  13. package/dist/canonical-json.js +5 -0
  14. package/dist/cli-main.d.ts +40 -3
  15. package/dist/cli-main.js +241 -62
  16. package/dist/command-table.d.ts +19 -11
  17. package/dist/command-table.js +190 -38
  18. package/dist/config/load.d.ts +16 -4
  19. package/dist/config/load.js +25 -3
  20. package/dist/config/unconfirmed-toml.d.ts +9 -0
  21. package/dist/config/unconfirmed-toml.js +29 -0
  22. package/dist/diagnostics.js +10 -1
  23. package/dist/docs/check.d.ts +16 -0
  24. package/dist/docs/check.js +197 -0
  25. package/dist/docs/decision-record.d.ts +34 -0
  26. package/dist/docs/decision-record.js +135 -0
  27. package/dist/docs/decisions.d.ts +16 -0
  28. package/dist/docs/decisions.js +103 -0
  29. package/dist/docs/markdown.d.ts +36 -0
  30. package/dist/docs/markdown.js +88 -0
  31. package/dist/docs/references.d.ts +32 -0
  32. package/dist/docs/references.js +148 -0
  33. package/dist/environment/application/confirm.d.ts +35 -0
  34. package/dist/environment/application/confirm.js +122 -0
  35. package/dist/environment/application/destructive.js +6 -2
  36. package/dist/environment/application/doctor.d.ts +12 -7
  37. package/dist/environment/application/doctor.js +20 -8
  38. package/dist/environment/application/guard.d.ts +12 -6
  39. package/dist/environment/application/guard.js +87 -32
  40. package/dist/environment/application/health.d.ts +14 -5
  41. package/dist/environment/application/health.js +8 -2
  42. package/dist/environment/application/lifecycle-context.d.ts +10 -3
  43. package/dist/environment/application/lifecycle-state.js +4 -3
  44. package/dist/environment/application/lifecycle.d.ts +2 -1
  45. package/dist/environment/application/lifecycle.js +11 -3
  46. package/dist/environment/application/services.d.ts +60 -0
  47. package/dist/environment/application/services.js +89 -0
  48. package/dist/environment/application/siblings.d.ts +43 -0
  49. package/dist/environment/application/siblings.js +269 -0
  50. package/dist/environment/claims.d.ts +10 -10
  51. package/dist/environment/claims.js +3 -11
  52. package/dist/environment/compose-topology.d.ts +15 -7
  53. package/dist/environment/compose-topology.js +100 -23
  54. package/dist/environment/contract.d.ts +42 -9
  55. package/dist/environment/contract.js +33 -0
  56. package/dist/environment/executable.d.ts +12 -0
  57. package/dist/environment/executable.js +46 -23
  58. package/dist/environment/generated-application/check.js +2 -0
  59. package/dist/environment/generated-application/prepare.js +2 -0
  60. package/dist/environment/generated-application/shared.d.ts +10 -2
  61. package/dist/environment/generated-application/shared.js +6 -1
  62. package/dist/environment/generated-application/surface-outputs.js +23 -12
  63. package/dist/environment/generated-application/surfaces.js +16 -11
  64. package/dist/environment/generated-application/sync.js +19 -4
  65. package/dist/environment/git-observation.d.ts +25 -6
  66. package/dist/environment/git-observation.js +37 -28
  67. package/dist/environment/health-composition.d.ts +3 -2
  68. package/dist/environment/health-composition.js +19 -2
  69. package/dist/environment/init-application.d.ts +39 -14
  70. package/dist/environment/init-application.js +39 -10
  71. package/dist/environment/lifecycle-composition.d.ts +4 -2
  72. package/dist/environment/lifecycle-composition.js +84 -17
  73. package/dist/environment/lifecycle-stage.d.ts +8 -3
  74. package/dist/environment/lifecycle-stage.js +11 -1
  75. package/dist/environment/logs.d.ts +1 -1
  76. package/dist/environment/map-limit.d.ts +2 -0
  77. package/dist/environment/map-limit.js +13 -0
  78. package/dist/environment/mcp-observation.d.ts +9 -0
  79. package/dist/environment/mcp-observation.js +46 -0
  80. package/dist/environment/observations.d.ts +1 -1
  81. package/dist/environment/observations.js +2 -0
  82. package/dist/environment/operator-application.d.ts +35 -6
  83. package/dist/environment/operator-application.js +42 -0
  84. package/dist/environment/presence.d.ts +4 -0
  85. package/dist/environment/presence.js +26 -0
  86. package/dist/environment/read-composition.d.ts +8 -3
  87. package/dist/environment/read-composition.js +53 -8
  88. package/dist/environment/readiness-composition.d.ts +20 -1
  89. package/dist/environment/readiness-composition.js +23 -2
  90. package/dist/environment/reclaim.d.ts +56 -19
  91. package/dist/environment/reclaim.js +147 -42
  92. package/dist/environment/runtime.d.ts +64 -18
  93. package/dist/environment/runtime.js +73 -3
  94. package/dist/environment/served-checkout-grants.d.ts +53 -0
  95. package/dist/environment/served-checkout-grants.js +182 -0
  96. package/dist/environment/sibling-checkouts.d.ts +119 -0
  97. package/dist/environment/sibling-checkouts.js +254 -0
  98. package/dist/environment/sibling-observation.d.ts +27 -0
  99. package/dist/environment/sibling-observation.js +45 -0
  100. package/dist/environment/sibling-pull.d.ts +23 -0
  101. package/dist/environment/sibling-pull.js +95 -0
  102. package/dist/environment/stack-inputs.d.ts +18 -0
  103. package/dist/environment/stack-inputs.js +210 -0
  104. package/dist/environment/stack-inventory-prune.d.ts +31 -0
  105. package/dist/environment/stack-inventory-prune.js +129 -0
  106. package/dist/environment/stack-inventory-report.d.ts +44 -0
  107. package/dist/environment/stack-inventory-report.js +78 -0
  108. package/dist/environment/stack-inventory.d.ts +94 -0
  109. package/dist/environment/stack-inventory.js +418 -0
  110. package/dist/environment/state.d.ts +48 -9
  111. package/dist/environment/state.js +142 -2
  112. package/dist/environment/tool-registration.js +3 -7
  113. package/dist/environment-exec/execute.d.ts +6 -3
  114. package/dist/environment-exec/execute.js +4 -0
  115. package/dist/fix-catalogue.d.ts +1 -1
  116. package/dist/generated-environment/adopt.js +4 -8
  117. package/dist/guards/answer-confirmation.d.ts +41 -0
  118. package/dist/guards/answer-confirmation.js +109 -0
  119. package/dist/guards/evaluator.d.ts +20 -3
  120. package/dist/guards/evaluator.js +39 -7
  121. package/dist/guards/local-identity.d.ts +6 -2
  122. package/dist/guards/local-identity.js +10 -4
  123. package/dist/guards/policy-change.d.ts +43 -0
  124. package/dist/guards/policy-change.js +159 -0
  125. package/dist/guards/policy-pin.d.ts +1 -1
  126. package/dist/guards/policy-pin.js +14 -12
  127. package/dist/init/command.d.ts +4 -3
  128. package/dist/init/command.js +36 -16
  129. package/dist/init/derive.d.ts +8 -6
  130. package/dist/init/derive.js +140 -36
  131. package/dist/init/draft.d.ts +13 -11
  132. package/dist/init/draft.js +54 -31
  133. package/dist/init/render.d.ts +18 -6
  134. package/dist/init/render.js +31 -9
  135. package/dist/repository-path.d.ts +9 -0
  136. package/dist/repository-path.js +16 -0
  137. package/dist/schema.d.ts +42 -75
  138. package/dist/schema.js +62 -11
  139. package/dist/surfaces/harness-registry.d.ts +80 -0
  140. package/dist/surfaces/harness-registry.js +29 -0
  141. package/dist/surfaces/mcp.d.ts +67 -17
  142. package/dist/surfaces/mcp.js +194 -31
  143. package/dist/surfaces/projection.d.ts +7 -7
  144. package/dist/surfaces/projection.js +41 -6
  145. package/dist/surfaces/render.d.ts +5 -44
  146. package/dist/surfaces/render.js +5 -2
  147. package/dist/team-metadata.js +2 -1
  148. package/dist/testing/test-protection.d.ts +34 -0
  149. package/dist/testing/test-protection.js +137 -0
  150. package/dist/testing/weakening.d.ts +15 -0
  151. package/dist/testing/weakening.js +30 -0
  152. package/dist/work-orders/application-closure.d.ts +7 -0
  153. package/dist/work-orders/application-closure.js +76 -0
  154. package/dist/work-orders/application-context.d.ts +162 -0
  155. package/dist/work-orders/application-context.js +137 -0
  156. package/dist/work-orders/application-decisions.d.ts +27 -0
  157. package/dist/work-orders/application-decisions.js +121 -0
  158. package/dist/work-orders/application-grading.d.ts +11 -0
  159. package/dist/work-orders/application-grading.js +212 -0
  160. package/dist/work-orders/application.d.ts +4 -97
  161. package/dist/work-orders/application.js +94 -254
  162. package/dist/work-orders/composition.d.ts +35 -58
  163. package/dist/work-orders/composition.js +216 -24
  164. package/dist/work-orders/contract.d.ts +22 -6
  165. package/dist/work-orders/contract.js +34 -16
  166. package/dist/work-orders/decisions.d.ts +23 -0
  167. package/dist/work-orders/decisions.js +55 -0
  168. package/dist/work-orders/store.d.ts +54 -5
  169. package/dist/work-orders/store.js +118 -24
  170. package/docs/README.md +6 -3
  171. package/docs/capability-matrix.md +17 -9
  172. package/docs/cli-reference.md +209 -1
  173. package/docs/command-list.md +12 -1
  174. package/docs/concepts.md +6 -4
  175. package/docs/configuration-essentials.md +7 -2
  176. package/docs/configuration-reference.md +18 -3
  177. package/docs/docs-check.md +76 -0
  178. package/docs/environment.md +194 -6
  179. package/docs/faq.md +5 -3
  180. package/docs/getting-started.md +109 -25
  181. package/docs/harness-surfaces.md +69 -2
  182. package/docs/safety-and-autonomy.md +56 -12
  183. package/docs/troubleshooting.md +29 -8
  184. package/docs/tutorial.md +3 -2
  185. package/docs/work-and-browser.md +180 -14
  186. package/libexec/browser-operation.cjs +86 -2
  187. package/package.json +2 -2
  188. package/surfaces/skills/cockpit-browser.skill.md +7 -2
  189. package/surfaces/skills/cockpit-explain.skill.md +1 -1
  190. package/surfaces/skills/cockpit-fix.skill.md +1 -1
  191. package/surfaces/skills/cockpit-setup.skill.md +3 -3
  192. package/surfaces/skills/cockpit-work.skill.md +1 -1
  193. package/surfaces/skills/procedures/explain.md +9 -4
  194. package/surfaces/skills/procedures/fix.md +25 -11
  195. package/surfaces/skills/procedures/setup.md +113 -40
  196. package/surfaces/skills/procedures/work.md +45 -16
package/dist/cli-main.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { EVALUATOR_COMMANDS, runningEvaluatorTool } from './schema.js';
2
+ import { ComposeProfileName, EVALUATOR_COMMANDS, runningEvaluatorTool, UNCONFIRMABLE_ANSWERS } from './schema.js';
3
3
  import { installedPackageMetadata } from './package-metadata.js';
4
4
  import { validateTeamMetadata } from './team-metadata.js';
5
5
  import { renderComposeScaffold, SCAFFOLD_BACKING_SERVICES, writeComposeScaffold } from './init/scaffold.js';
6
6
  import { discoverReleaseFreshness } from './freshness.js';
7
+ import { observeMcpStartup } from './environment/mcp-observation.js';
7
8
  import { lstat, mkdir, realpath, writeFile } from 'node:fs/promises';
8
9
  import { parse as parseToml, TomlError } from 'smol-toml';
9
10
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
@@ -13,6 +14,7 @@ import { createInvocationRecord, EXIT_CODES } from './invocation.js';
13
14
  import { commandGroups, commandSpelling, commandStages, commands, describePublicOption, internalCommands, isWorkHandler, isRecordedAnswerField, resolveOrdinaryInvocation } from './command-table.js';
14
15
  import { SURFACE_TARGETS } from './surfaces/contract.js';
15
16
  import { DEFAULT_SURFACE_TOOLS, GUARD_SURFACE_TARGETS, isGuardSurfaceTarget } from './surfaces/harness-registry.js';
17
+ import { cockpitCommand } from './fix-catalogue.js';
16
18
  import { renderHumanDiagnostics, renderHumanValue, renderJsonResponse } from './diagnostics.js';
17
19
  import { classifyEstateObservation, draftFromEstate, observeCockpitRoot, observePublicEstate } from './init/draft.js';
18
20
  import { runInitOperatorCommand } from './init/command.js';
@@ -25,25 +27,31 @@ import { beginOperatorReset as beginOperatorResetApplication, runDown as runDown
25
27
  import { runDoctor as runDoctorApplication } from './environment/application/doctor.js';
26
28
  import { runGuardCommand } from './cli-guard.js';
27
29
  import { runPreflight as runPreflightApplication } from './environment/application/health.js';
30
+ import { runConfirm } from './environment/application/confirm.js';
31
+ import { isUnconfirmedAnswerName } from './guards/answer-confirmation.js';
28
32
  import { runSession as runSessionApplication, runSleep as runSleepApplication, runUp as runUpApplication, } from './environment/application/lifecycle.js';
29
33
  import {} from './environment/application/lifecycle-context.js';
30
34
  import { runLogs as runLogsApplication } from './environment/application/logs.js';
31
- import { executeForegroundProcess, executePlan, executeProbe } from './environment-exec/execute.js';
35
+ import { runDocsCheck } from './docs/check.js';
36
+ import { runServiceExec, runServicePs, runServiceRestart } from './environment/application/services.js';
37
+ import { executeForegroundProcess, executePlan, executeProbe, foregroundTerminal } from './environment-exec/execute.js';
32
38
  import { createDestructiveCommandAdapters, selectDestructiveDaemon } from './environment/destructive-composition.js';
33
39
  import { createHealthCommandAdapters } from './environment/health-composition.js';
34
- import { inspectInitReadiness } from './environment/readiness-composition.js';
40
+ import { inspectInitReadiness, measureInitMachine } from './environment/readiness-composition.js';
35
41
  import { createLifecycleCommandAdapters } from './environment/lifecycle-composition.js';
36
42
  import { isProcessStart, observeProcessStart, processHost } from './environment/claims.js';
37
43
  import { createReadCommandAdapters } from './environment/read-composition.js';
38
44
  import { probeInstalledRuntime, runRuntimeMeasure } from './environment/runtime-measurement.js';
39
45
  import { initializeLocalGitRepository, runSiblingAdd, runSiblingRemove } from './environment/sibling-registration.js';
40
46
  import { runToolChange } from './environment/tool-registration.js';
47
+ import { runPull, runUse } from './environment/application/siblings.js';
41
48
  import { issueSetupRunContext, setupEnvironment } from './environment/application/setup.js';
42
49
  import { consumeSetupCheckoutProjection, observeEnvironmentIgnore, observeSetupCheckout, observeSetupPath, withSetupCheckoutProjection, } from './environment/setup-observation.js';
43
50
  import { RefusalError, ENVIRONMENT_CODES, errorCause, hasErrorCode, INHERITED_TOOL_ENVIRONMENT, isCanonicalTimestamp, } from './environment/contract.js';
44
51
  import { refusalOutcome } from './refusal.js';
45
52
  import { loadAutonomyPolicy, loadCockpitConfig, observeConfigSource, readBoundedInput } from './config/load.js';
46
53
  import { policyWeakening, policyWeakeningDiagnostic } from './autonomy.js';
54
+ import { inspectGuardConfigChange } from './guards/policy-change.js';
47
55
  import { gitEnvironment, planSetup } from './environment/setup.js';
48
56
  import { synchronizeInstructionPointer } from './environment/instruction-pointer.js';
49
57
  import { synchronizeSiblingInstructionFiles } from './environment/sibling-instructions.js';
@@ -58,7 +66,9 @@ import { prepareSelectedBootstrapSurface, resumeSelectedBootstrapSurface, applyS
58
66
  import { runUpgrade } from './environment/upgrade-application.js';
59
67
  import { inspectCodexHookTrust } from './environment/codex-hook-trust.js';
60
68
  import { runConfiguredSurfaceSelftestOutcome, runSelftestOperatorCommand, } from './environment/selftest-application.js';
61
- import { loadOperatorPolicy, runResetOperatorApplication } from './environment/operator-application.js';
69
+ import { loadOperatorPolicy, runResetOperatorApplication, runStacksPruneOperatorApplication } from './environment/operator-application.js';
70
+ import { beginStacksPrune } from './environment/stack-inventory-prune.js';
71
+ import { runStacksInventory } from './environment/stack-inventory-report.js';
62
72
  function setupDiagnostic(code, message, target) {
63
73
  return { severity: 'error', code, message, target };
64
74
  }
@@ -260,7 +270,7 @@ function readAdapters(configPath, runtime, sessionId) {
260
270
  sessionId,
261
271
  timeoutMs: RUNTIME_STEP_TIMEOUT_MS,
262
272
  ...(runtime.signal ? { signal: runtime.signal } : {}),
263
- }, executeProbe);
273
+ }, executeProbe, executeForegroundProcess);
264
274
  }
265
275
  // Each return is a literal, so the diagnostic inventory captures the fix beside its code.
266
276
  function unsatisfiedWorkDiagnostic(operation, message) {
@@ -268,6 +278,10 @@ function unsatisfiedWorkDiagnostic(operation, message) {
268
278
  return { severity: 'error', message,
269
279
  code: 'TEST_GATE_UNSATISFIED',
270
280
  fix: 'the message gives the verdict and its reasons. FAIL: a check a stage ran failed, so fix the product. DEGRADED: start the stack with npx --no cockpit up --config <your cockpit.toml> for a stage that needs it, make a stage command that could not start runnable from the cockpit root, raise testing.stages[].timeout_seconds for a stage that timed out, or declare testing.stages[].select.<kind> for a --select a stage lacks. VOID: stop changing the checkouts and run the gate again. PARTIAL: only the complete gate can PASS, so run it without --select or --mode staged' };
281
+ if (operation === 'browser-drive')
282
+ return { severity: 'error', message,
283
+ code: 'BROWSER_ROUTE_FINDINGS',
284
+ fix: 'this run asked for a clean route (--fail-on-findings). Read result.receipt.surfaces[].diagnostics, including excerpt.messages where present: fix the product for each console error, uncaught page error and failed request that matters, then rerun npx --no cockpit browser drive --config <your cockpit.toml> --route <name> --fail-on-findings; third-party traffic never counts' };
271
285
  if (operation === 'work-check')
272
286
  return { severity: 'error', message,
273
287
  code: 'WORK_CHECK_FAILED',
@@ -275,14 +289,19 @@ function unsatisfiedWorkDiagnostic(operation, message) {
275
289
  if (operation === 'work-verify')
276
290
  return { severity: 'error', message,
277
291
  code: 'WORK_VERIFY_FAILED',
278
- fix: 'for an order changed since acceptance, run npx --no cockpit work accept --config <your cockpit.toml> --work-order <id> or revert the change; for a done order without a passing grade, regrade it with work grade' };
279
- if (operation === 'grade-criteria')
280
- return { severity: 'error', message,
281
- code: 'WORK_GRADE_UNSATISFIED',
282
- fix: 'read result.criteria, result.checks and result.outOfScope: fix the product for an unmet or unclear criterion, make a failed check pass, move or revert a change outside scope, then grade again with npx --no cockpit grade --config <your cockpit.toml> --criteria <file>' };
292
+ fix: 'for an order changed since acceptance, run npx --no cockpit work accept --config <your cockpit.toml> --work-order <id> or revert the change; for a done order without a passing grade, regrade it with work grade; for a decision record that is missing or no longer cites its order, restore it from git or put back its "Work order: <id>" line' };
283
293
  return { severity: 'error', message,
284
294
  code: 'WORK_GRADE_UNSATISFIED',
285
- fix: 'read result.criteria, result.checks and result.outOfScope: fix the product for an unmet or unclear criterion, make a failed check pass, move or revert a change outside scope, then grade again with npx --no cockpit work grade --config <your cockpit.toml> --work-order <id>' };
295
+ fix: operation === 'grade-criteria'
296
+ ? 'read result.criteria and result.checks: fix the product for an unmet or unclear criterion, make a failed check pass, revert each change outside scope the scope-changes-necessary verdict calls unneeded, then grade again with npx --no cockpit grade --config <your cockpit.toml> --criteria <file>'
297
+ : 'read result.criteria, result.checks and result.outOfScope: fix the product for an unmet or unclear criterion, make a failed check pass, revert each change outside scope the scope-changes-necessary verdict calls unneeded (under [work] grading = "checks", every change outside scope), have a person amend the order when amendments-hold-bar is unmet, then grade again with npx --no cockpit work grade --config <your cockpit.toml> --work-order <id>' };
298
+ }
299
+ function weakenedTestsDiagnostic(operation, message) {
300
+ return { severity: 'error', message,
301
+ code: 'WORK_TEST_WEAKENED',
302
+ fix: operation === 'work-grade'
303
+ ? 'read result.testProtection.findings and the verdict on them: restore each test weakened to make the change pass. Under [work] grading = "independent" the evaluator judged the test changes unjustified in the diff; under "self" your --note names each flagged file and why; under "checks" acknowledge a legitimate change with npx --no cockpit work acknowledge --config <your cockpit.toml> --work-order <id> --note <why it is not a weakening>, which [work.gates] acknowledge = "person" leaves to a person at a terminal with --interactive; never type the digest for them'
304
+ : 'read result.testProtection: restore each deleted, skipped, suppressed or loosened test. A grade without an order has the evaluator judge the test changes; an order records the base they are compared with, and under [work] grading = "checks" a legitimate change is acknowledged with npx --no cockpit work acknowledge --config <your cockpit.toml> --work-order <id> --note <why>, with --interactive by a person where [work.gates] acknowledge = "person"' };
286
305
  }
287
306
  // A person gate binds what the person reads to the digest they type, so it never completes for an agent.
288
307
  function personRequired(handler, prompt) {
@@ -302,8 +321,12 @@ function sameToolGradeWarnings(value, env) {
302
321
  function workCommandOutcome(outcome, env) {
303
322
  if (outcome.kind === 'complete')
304
323
  return { status: 0, result: outcome.value, diagnostics: sameToolGradeWarnings(outcome.value, env) };
305
- if (outcome.kind === 'unsatisfied')
306
- return { status: 2, result: outcome.value, diagnostics: [unsatisfiedWorkDiagnostic(outcome.operation, outcome.reason), ...sameToolGradeWarnings(outcome.value, env)] };
324
+ if (outcome.kind === 'unsatisfied') {
325
+ const weakened = outcome.weakening ? [weakenedTestsDiagnostic(outcome.operation, outcome.weakening)] : [];
326
+ // A failure that is only test weakening is already stated by its own diagnostic.
327
+ const unsatisfied = weakened.length === 0 || outcome.reason !== '' ? [unsatisfiedWorkDiagnostic(outcome.operation, outcome.reason)] : [];
328
+ return { status: 2, result: outcome.value, diagnostics: [...weakened, ...unsatisfied, ...sameToolGradeWarnings(outcome.value, env)] };
329
+ }
307
330
  return usageOutcome('WORK_APPLICATION_REFUSED', outcome.kind === 'refusal' ? outcome.reason : 'this step needs a person at a terminal');
308
331
  }
309
332
  async function configuredWorkApplication(input, runtime) {
@@ -358,18 +381,23 @@ function defaultHandlers(runtime) {
358
381
  const env = runtime.env ?? process.env;
359
382
  const inherited = inheritedSessionId(env);
360
383
  const ordinarySessionId = inherited === undefined ? 'cockpit-cli' : inherited;
361
- const lifecycleAdapters = (configPath, sessionId, holderProcess) => createLifecycleCommandAdapters({
384
+ const lifecycleAdapters = (configPath, sessionId, holderProcess, profiles = []) => createLifecycleCommandAdapters({
362
385
  configPath,
363
386
  env,
364
387
  sessionId,
365
388
  timeoutMs: RUNTIME_STEP_TIMEOUT_MS,
366
389
  ...(holderProcess === undefined ? {} : { holderProcess }),
390
+ activeProfiles: profiles,
367
391
  }, executeProbe);
368
392
  const healthAdapters = (configPath) => createHealthCommandAdapters({
369
393
  configPath,
370
394
  env,
371
395
  timeoutMs: RUNTIME_STEP_TIMEOUT_MS,
372
396
  }, executeProbe);
397
+ // The ordinary (non-session) verbs all need a valid session identity and an absolute config path.
398
+ const withSession = (run) => async (input) => ordinarySessionId === null
399
+ ? usageOutcome('CLI_USAGE_INVALID', 'COCKPIT_SESSION_ID must be a canonical UUID', 'COCKPIT_SESSION_ID')
400
+ : run(input, resolve(input.configPath), ordinarySessionId);
373
401
  const workHandlers = {};
374
402
  for (const command of commands)
375
403
  if (isWorkHandler(command.handler)) {
@@ -388,6 +416,7 @@ function defaultHandlers(runtime) {
388
416
  infer: deriveInferredFacts,
389
417
  composeFindings,
390
418
  inspectReadiness: input => inspectInitReadiness(input, { env, timeoutMs: RUNTIME_STEP_TIMEOUT_MS, execute: executeProbe }),
419
+ measureMachine: input => measureInitMachine(input, { env, timeoutMs: RUNTIME_STEP_TIMEOUT_MS, execute: executeProbe }),
391
420
  observeDestination: async ({ path, byteLimit }) => {
392
421
  const node = await nodeAuthority();
393
422
  const read = node ? await readAnchoredBatches(dirname(path), [
@@ -426,36 +455,28 @@ function defaultHandlers(runtime) {
426
455
  // A weakening sync would refuse is a failing check, so an agent's edit cannot sit unreported.
427
456
  const policy = await loadAutonomyPolicy(input.configPath);
428
457
  const weakened = 'recorded' in policy && policy.recorded ? policyWeakening(policy.recorded, policy.current) : [];
458
+ // An edit the guards were not regenerated for holds the agent; one that widens them waits for a person.
459
+ const guardChange = await inspectGuardConfigChange(dirname(loaded.result.source.path), loaded.result.source.path, { sha256: loaded.result.source.sha256, config: loaded.result.config });
429
460
  // Codex hook trust concerns a Codex user only; a leftover hooks.json of an unlisted Codex is stale, which check reports.
430
461
  const [outcome, hookTrust] = await Promise.all([checkGeneratedEnvironment(input),
431
462
  loaded.result.config.surfaces.tools.includes('codexcli')
432
463
  ? inspectCodexHookTrust({ root: dirname(resolve(input.configPath)), env }) : Promise.resolve([])]);
433
- if (weakened.length)
464
+ if (weakened.length || guardChange.some(row => row.severity === 'error'))
434
465
  return { kind: 'refusal', status: 2, result: null,
435
- diagnostics: [policyWeakeningDiagnostic(weakened), ...outcome.diagnostics, ...hookTrust] };
436
- return { ...outcome, diagnostics: [...outcome.diagnostics, ...hookTrust], result: outcome.report
466
+ diagnostics: [...weakened.length ? [policyWeakeningDiagnostic(weakened)] : [], ...guardChange, ...outcome.diagnostics, ...hookTrust] };
467
+ return { ...outcome, diagnostics: [...guardChange, ...outcome.diagnostics, ...hookTrust], result: outcome.report
437
468
  ? outcome.kind === 'complete' ? { ...outcome.result, currentness: outcome.report } : outcome.report
438
469
  : outcome.result };
439
470
  },
440
471
  selftest: runConfiguredSurfaceSelftestOutcome,
441
472
  upgrade: input => runUpgrade({ ...input, env: concreteEnvironment(env) }, { execute: executeProbe, executeForeground: executeForegroundProcess }),
442
- up: async (input) => {
443
- const sessionId = ordinarySessionId;
444
- if (sessionId === null)
445
- return usageOutcome('CLI_USAGE_INVALID', 'COCKPIT_SESSION_ID must be a canonical UUID', 'COCKPIT_SESSION_ID');
473
+ up: withSession(async (input, configPath, sessionId) => {
446
474
  const wrapper = await sessionChildHolderProcess(env);
447
475
  if (wrapper.refusal)
448
476
  return wrapper.refusal;
449
- const configPath = resolve(input.configPath);
450
- return runUpApplication({ configPath }, lifecycleAdapters(configPath, sessionId, wrapper.holderProcess));
451
- },
452
- sleep: (input) => {
453
- const sessionId = ordinarySessionId;
454
- if (sessionId === null)
455
- return Promise.resolve(usageOutcome('CLI_USAGE_INVALID', 'COCKPIT_SESSION_ID must be a canonical UUID', 'COCKPIT_SESSION_ID'));
456
- const configPath = resolve(input.configPath);
457
- return runSleepApplication({ configPath }, lifecycleAdapters(configPath, sessionId));
458
- },
477
+ return runUpApplication({ configPath }, lifecycleAdapters(configPath, sessionId, wrapper.holderProcess, input.profiles));
478
+ }),
479
+ sleep: withSession((_input, configPath, sessionId) => runSleepApplication({ configPath }, lifecycleAdapters(configPath, sessionId))),
459
480
  session: async (input) => {
460
481
  if (inherited === null)
461
482
  return usageOutcome('CLI_USAGE_INVALID', 'COCKPIT_SESSION_ID must be a canonical UUID', 'COCKPIT_SESSION_ID');
@@ -473,8 +494,9 @@ function defaultHandlers(runtime) {
473
494
  sessionId,
474
495
  timeoutMs: RUNTIME_STEP_TIMEOUT_MS,
475
496
  holderProcess: { pid: process.pid, ...(started.state === 'present' ? { processStart: started.start } : {}) },
497
+ activeProfiles: input.profiles,
476
498
  }, executeProbe);
477
- return runSessionApplication({ ...input, configPath, cwd: process.cwd(), env: childEnv }, {
499
+ return runSessionApplication({ configPath, argv: input.argv, cwd: process.cwd(), env: childEnv }, {
478
500
  up: (request) => runUpApplication(request, adapters),
479
501
  sleep: (request) => runSleepApplication(request, adapters),
480
502
  resolveExecutable: (name) => resolveExecutable(name, sessionEnvironment),
@@ -482,18 +504,8 @@ function defaultHandlers(runtime) {
482
504
  ...(runtime.signal ? { signal: runtime.signal } : {}),
483
505
  });
484
506
  },
485
- down: async (input) => {
486
- if (ordinarySessionId === null)
487
- return usageOutcome('CLI_USAGE_INVALID', 'COCKPIT_SESSION_ID must be a canonical UUID', 'COCKPIT_SESSION_ID');
488
- const configPath = resolve(input.configPath);
489
- return runDownApplication({ configPath }, await destructiveAdapters(configPath, runtime, ordinarySessionId));
490
- },
491
- reset: async (input) => {
492
- if (ordinarySessionId === null)
493
- return usageOutcome('CLI_USAGE_INVALID', 'COCKPIT_SESSION_ID must be a canonical UUID', 'COCKPIT_SESSION_ID');
494
- const configPath = resolve(input.configPath);
495
- return runResetApplication({ configPath }, await destructiveAdapters(configPath, runtime, ordinarySessionId));
496
- },
507
+ down: withSession(async (_input, configPath, sessionId) => runDownApplication({ configPath }, await destructiveAdapters(configPath, runtime, sessionId))),
508
+ reset: withSession(async (_input, configPath, sessionId) => runResetApplication({ configPath }, await destructiveAdapters(configPath, runtime, sessionId))),
497
509
  preflight: (input) => {
498
510
  const configPath = resolve(input.configPath);
499
511
  return runPreflightApplication({ configPath }, healthAdapters(configPath));
@@ -504,19 +516,36 @@ function defaultHandlers(runtime) {
504
516
  ...healthAdapters(configPath),
505
517
  discoverReleaseFreshness: () => discoverReleaseFreshness({ verb: 'doctor', env,
506
518
  ...(runtime.createReleaseMetadataTransport ? { createTransport: runtime.createReleaseMetadataTransport } : {}) }),
519
+ inspectMcpStartup: servers => observeMcpStartup(servers, env),
507
520
  });
508
521
  },
509
522
  runtime: (input) => runRuntimeMeasure({ ...input, env }, { execute: executeProbe, probe: probeInstalledRuntime }),
523
+ stacks: (input) => runStacksInventory({ configPath: resolve(input.configPath), env }, { execute: executeProbe }),
524
+ 'docs-check': (input) => runDocsCheck({ configPath: resolve(input.configPath), env }, { execute: executeProbe }),
525
+ confirm: (input) => {
526
+ const configPath = resolve(input.configPath);
527
+ return runConfirm({ configPath, answer: input.answer }, {
528
+ preflight: ({ configPath }) => runPreflightApplication({ configPath }, healthAdapters(configPath)),
529
+ check: async ({ configPath }) => {
530
+ const metadata = teamMetadataOutcome(await validateTeamMetadata(configPath));
531
+ return metadata ?? checkGeneratedEnvironment({ configPath });
532
+ },
533
+ now: () => new Date(),
534
+ });
535
+ },
510
536
  sibling: (request) => request.operation === 'add'
511
537
  ? runSiblingAdd({ ...request.input, env }, { execute: executeProbe })
512
538
  : runSiblingRemove({ ...request.input, env }),
513
539
  tool: runToolChange,
514
- logs: (input) => {
515
- if (ordinarySessionId === null)
516
- return Promise.resolve(usageOutcome('CLI_USAGE_INVALID', 'COCKPIT_SESSION_ID must be a canonical UUID', 'COCKPIT_SESSION_ID'));
517
- const configPath = resolve(input.configPath);
518
- return runLogsApplication({ ...input, configPath }, readAdapters(configPath, runtime, ordinarySessionId).logs);
519
- },
540
+ use: (input) => runUse({ ...input, env }, { execute: executeProbe }),
541
+ pull: (input) => runPull({ ...input, env }, { execute: executeProbe }),
542
+ logs: withSession((input, configPath, sessionId) => runLogsApplication({ ...input, configPath }, readAdapters(configPath, runtime, sessionId).logs)),
543
+ ps: withSession((_input, configPath, sessionId) => runServicePs({ configPath }, readAdapters(configPath, runtime, sessionId).services)),
544
+ restart: withSession((input, configPath, sessionId) => runServiceRestart({ configPath, service: input.service, verb: 'restart' }, readAdapters(configPath, runtime, sessionId).services)),
545
+ rebuild: withSession((input, configPath, sessionId) => runServiceRestart({ configPath, service: input.service, verb: 'rebuild' }, readAdapters(configPath, runtime, sessionId).services)),
546
+ // A terminal on both ends gets an interactive shell or client; anything else, an agent's
547
+ // captured output included, gets plain streams.
548
+ exec: withSession((input, configPath, sessionId) => runServiceExec({ configPath, service: input.service, argv: input.argv, tty: foregroundTerminal() }, readAdapters(configPath, runtime, sessionId).services)),
520
549
  };
521
550
  }
522
551
  function commandLines(rows, width = Math.max(...rows.map(row => row.name.length))) {
@@ -539,8 +568,9 @@ function renderHelp() {
539
568
  const width = Math.max(...entries.map(entry => entry.name.length));
540
569
  const sections = commandStages.map(stage => [`${stage.title}:`,
541
570
  ...commandLines(entries.filter(entry => entry.stage === stage.id), width)].join('\n'));
571
+ const groups = commandGroups.map(group => `'cockpit ${group.name} --help'`);
542
572
  return ['Usage: cockpit <command> [options]', '', sections.join('\n\n'), '',
543
- `Run 'cockpit <command> --help' for its options, and ${commandGroups.map(group => `'cockpit ${group.name} --help'`).join(' or ')} for its subcommands.`, ''].join('\n');
573
+ `Run 'cockpit <command> --help' for its options, and ${[groups.slice(0, -1).join(', '), ...groups.slice(-1)].filter(Boolean).join(' or ')} for its subcommands.`, ''].join('\n');
544
574
  }
545
575
  function renderGroupHelp(group) {
546
576
  const members = groupMembers(group.name);
@@ -563,6 +593,15 @@ function renderCommandHelp(definition) {
563
593
  function renderVersion() {
564
594
  return `${installedPackageMetadata.name} ${installedPackageMetadata.version} (Node ${installedPackageMetadata.engines.node}; source ${installedPackageMetadata.cockpitSourceRevision})\n`;
565
595
  }
596
+ // Repeated --profile values, each in the spelling environment.profiles accepts, or why not.
597
+ function composeProfiles(values) {
598
+ const profiles = values ?? [];
599
+ const invalid = profiles.find(profile => !ComposeProfileName.safeParse(profile).success);
600
+ if (invalid !== undefined)
601
+ return { error: `--profile ${JSON.stringify(invalid)} must be a lowercase Compose profile name: letters, digits, '.', '_' and '-', starting with a letter or digit` };
602
+ return { profiles: [...new Set(profiles)] };
603
+ }
604
+ const namedOnly = (handler, positionals) => positionals.length > 0 ? usageOutcome('CLI_USAGE_INVALID', `${handler} accepts named inputs only`) : undefined;
566
605
  const missingConfig = () => usageOutcome('CLI_USAGE_INVALID', 'provide --config <file>', '--config');
567
606
  function usageOutcome(code, message, target) {
568
607
  return {
@@ -743,7 +782,24 @@ async function decodeRequest(handler, parsed, runtimeEnv) {
743
782
  const note = text('text');
744
783
  if (!note)
745
784
  return usageOutcome('CLI_USAGE_INVALID', 'provide --text <note>', '--text');
746
- request = { operation: handler, workOrderId: workOrderId, text: note, question: values.ask === true };
785
+ if (values.ask === true && values.decision === true)
786
+ return usageOutcome('CLI_USAGE_INVALID', 'pass at most one of --ask and --decision');
787
+ request = { operation: handler, workOrderId: workOrderId, text: note, kind: values.ask === true ? 'question' : values.decision === true ? 'decision' : 'note' };
788
+ }
789
+ else if (handler === 'work-decide') {
790
+ const decision = Number(text('decision')), file = text('record-file'), supersedes = text('supersedes'), reason = text('reason');
791
+ const record = values.record === true || file !== undefined;
792
+ if (!Number.isSafeInteger(decision) || decision < 1 || record === (values.none === true)) {
793
+ return usageOutcome('CLI_USAGE_INVALID', 'provide --decision <number> and one of --record, --record-file <path> or --none --reason <why>');
794
+ }
795
+ if (values.record === true && file !== undefined)
796
+ return usageOutcome('CLI_USAGE_INVALID', 'pass --record to write a new record or --record-file <path> to cite one, not both');
797
+ if (values.none === true && (!reason || supersedes !== undefined))
798
+ return usageOutcome('CLI_USAGE_INVALID', '--none needs --reason <why the decision is not lasting> and takes no --supersedes', '--reason');
799
+ if (record && reason !== undefined)
800
+ return usageOutcome('CLI_USAGE_INVALID', '--reason goes with --none', '--reason');
801
+ request = { operation: handler, workOrderId: workOrderId, decision, resolution: values.none === true ? { kind: 'none', reason: reason }
802
+ : { kind: 'record', ...file !== undefined ? { path: file } : {}, ...supersedes !== undefined ? { supersedes } : {} } };
747
803
  }
748
804
  else if (handler === 'work-answer') {
749
805
  const answer = text('text'), question = Number(text('question'));
@@ -751,6 +807,12 @@ async function decodeRequest(handler, parsed, runtimeEnv) {
751
807
  return usageOutcome('CLI_USAGE_INVALID', 'provide --question <number> and --text <answer>');
752
808
  request = { operation: handler, workOrderId: workOrderId, question, text: answer };
753
809
  }
810
+ else if (handler === 'work-acknowledge') {
811
+ const note = text('note');
812
+ if (!note)
813
+ return usageOutcome('CLI_USAGE_INVALID', 'provide --note <why this change to the tests is not a weakening>', '--note');
814
+ request = { operation: handler, workOrderId: workOrderId, note };
815
+ }
754
816
  else if (handler === 'work-grade') {
755
817
  const verdict = text('verdict'), note = text('note');
756
818
  if (verdict !== undefined && verdict !== 'met' && verdict !== 'unmet')
@@ -791,7 +853,10 @@ async function decodeRequest(handler, parsed, runtimeEnv) {
791
853
  const role = text('role');
792
854
  if (values.role !== undefined && !role)
793
855
  return usageOutcome('CLI_USAGE_INVALID', '--role requires a bounded identifier');
794
- request = { operation: handler, ...(role ? { role } : {}) };
856
+ const route = text('route');
857
+ if (values.route !== undefined && !route)
858
+ return usageOutcome('CLI_USAGE_INVALID', '--route requires a declared route name or path', '--route');
859
+ request = { operation: handler, ...(role ? { role } : {}), ...(route ? { route } : {}), ...(values['fail-on-findings'] === true ? { failOnFindings: true } : {}) };
795
860
  }
796
861
  const { captureWorkApplicationRequest } = await import('./work-orders/application.js');
797
862
  const captured = captureWorkApplicationRequest(request);
@@ -955,6 +1020,10 @@ async function decodeRequest(handler, parsed, runtimeEnv) {
955
1020
  const [command, ...argv] = parsed.positionals;
956
1021
  if (!values.config)
957
1022
  return missingConfig();
1023
+ const composed = composeProfiles(values.profile);
1024
+ if ('error' in composed)
1025
+ return usageOutcome('CLI_USAGE_INVALID', composed.error, '--profile');
1026
+ const { profiles } = composed;
958
1027
  if (!command)
959
1028
  return usageOutcome('CLI_USAGE_INVALID', 'session requires a command after --', 'session');
960
1029
  const inherited = inheritedSessionId(runtimeEnv);
@@ -962,17 +1031,40 @@ async function decodeRequest(handler, parsed, runtimeEnv) {
962
1031
  return usageOutcome('CLI_USAGE_INVALID', 'COCKPIT_SESSION_ID must be a canonical UUID', 'COCKPIT_SESSION_ID');
963
1032
  if (inherited !== undefined)
964
1033
  return usageOutcome('CLI_USAGE_INVALID', 'session cannot be nested inside another supervised session', 'session');
965
- return { handler, input: { configPath: values.config, argv: [command, ...argv], sessionId: randomUUID() } };
1034
+ return { handler, input: { configPath: values.config, argv: [command, ...argv], sessionId: randomUUID(), profiles } };
966
1035
  }
967
- if (handler === 'check' || handler === 'up' || handler === 'sleep' || handler === 'down' || handler === 'reset'
968
- || handler === 'preflight' || handler === 'doctor') {
1036
+ if (handler === 'up') {
969
1037
  const values = parsed.values;
970
- if (parsed.positionals.length > 0) {
971
- return usageOutcome('CLI_USAGE_INVALID', `${handler} accepts named inputs only`);
972
- }
1038
+ if (parsed.positionals.length > 0)
1039
+ return usageOutcome('CLI_USAGE_INVALID', 'up accepts named inputs only');
973
1040
  if (!values.config)
974
1041
  return missingConfig();
975
- return { handler, input: { configPath: values.config } };
1042
+ const composed = composeProfiles(values.profile);
1043
+ if ('error' in composed)
1044
+ return usageOutcome('CLI_USAGE_INVALID', composed.error, '--profile');
1045
+ const { profiles } = composed;
1046
+ return { handler, input: { configPath: values.config, profiles } };
1047
+ }
1048
+ if (handler === 'ps' || handler === 'restart' || handler === 'rebuild' || handler === 'exec') {
1049
+ const values = parsed.values;
1050
+ if (!values.config)
1051
+ return missingConfig();
1052
+ if (handler === 'ps')
1053
+ return namedOnly(handler, parsed.positionals) ?? { handler, input: { configPath: values.config } };
1054
+ if (!values.service)
1055
+ return usageOutcome('CLI_USAGE_INVALID', 'provide --service <name>', '--service');
1056
+ if (handler === 'exec') {
1057
+ const [command, ...argv] = parsed.positionals;
1058
+ if (!command)
1059
+ return usageOutcome('CLI_USAGE_INVALID', 'exec requires a command after --', 'exec');
1060
+ return { handler, input: { configPath: values.config, service: values.service, argv: [command, ...argv] } };
1061
+ }
1062
+ return namedOnly(handler, parsed.positionals) ?? { handler, input: { configPath: values.config, service: values.service } };
1063
+ }
1064
+ if (handler === 'check' || handler === 'sleep' || handler === 'down' || handler === 'reset'
1065
+ || handler === 'preflight' || handler === 'doctor' || handler === 'docs-check') {
1066
+ const values = parsed.values;
1067
+ return namedOnly(handler, parsed.positionals) ?? (values.config ? { handler, input: { configPath: values.config } } : missingConfig());
976
1068
  }
977
1069
  if (handler === 'logs') {
978
1070
  const values = parsed.values;
@@ -1000,6 +1092,16 @@ async function decodeRequest(handler, parsed, runtimeEnv) {
1000
1092
  },
1001
1093
  };
1002
1094
  }
1095
+ if (handler === 'confirm') {
1096
+ const values = parsed.values;
1097
+ const [answer, ...extra] = parsed.positionals;
1098
+ if (answer === undefined || extra.length > 0 || !isUnconfirmedAnswerName(answer)) {
1099
+ return usageOutcome('CLI_USAGE_INVALID', `confirm takes one answer listed under unconfirmed, one of ${UNCONFIRMABLE_ANSWERS.join(', ')}: ${cockpitCommand('confirm', { operand: 'ports.bases' })}`);
1100
+ }
1101
+ if (!values.config)
1102
+ return missingConfig();
1103
+ return { handler, input: { configPath: values.config, answer } };
1104
+ }
1003
1105
  if (handler === 'runtime') {
1004
1106
  const values = parsed.values;
1005
1107
  const [operation, ...extra] = parsed.positionals;
@@ -1012,6 +1114,21 @@ async function decodeRequest(handler, parsed, runtimeEnv) {
1012
1114
  return usageOutcome('CLI_USAGE_INVALID', 'provide --image <local-image>; the probe never pulls', '--image');
1013
1115
  return { handler, input: { configPath: values.config, image: values.image } };
1014
1116
  }
1117
+ if (handler === 'stacks') {
1118
+ const values = parsed.values;
1119
+ const [operation, ...extra] = parsed.positionals;
1120
+ if (operation !== undefined && operation !== 'prune' || extra.length > 0) {
1121
+ return usageOutcome('CLI_USAGE_INVALID', `stacks takes no operation, or prune: ${cockpitCommand('stacks')}, or ${cockpitCommand('stacks prune', { args: '[--apply | --interactive]' })}`);
1122
+ }
1123
+ if (operation === undefined && (values.apply || values.interactive)) {
1124
+ return usageOutcome('CLI_USAGE_INVALID', '--apply and --interactive belong to stacks prune; stacks alone only lists', values.apply ? '--apply' : '--interactive');
1125
+ }
1126
+ if (values.apply && values.interactive)
1127
+ return usageOutcome('CLI_USAGE_INVALID', 'choose --apply or --interactive, not both', '--apply');
1128
+ if (!values.config)
1129
+ return missingConfig();
1130
+ return { handler, input: { configPath: values.config, operation: operation === 'prune' ? 'prune' : 'list' } };
1131
+ }
1015
1132
  if (handler === 'sibling') {
1016
1133
  const values = parsed.values;
1017
1134
  const [operation, slug, ...extra] = parsed.positionals;
@@ -1036,6 +1153,26 @@ async function decodeRequest(handler, parsed, runtimeEnv) {
1036
1153
  ...(values['instruction-pointer'] === true ? { instructionPointer: true } : {}),
1037
1154
  } } };
1038
1155
  }
1156
+ if (handler === 'use') {
1157
+ const values = parsed.values;
1158
+ const [slug, checkout, ...extra] = parsed.positionals;
1159
+ const shape = 'npx --no cockpit use <slug> <checkout> --config <your cockpit.toml>, or npx --no cockpit use <slug> --clear --config <your cockpit.toml>';
1160
+ if (!slug || extra.length > 0 || (checkout === undefined) === (values.clear !== true)) {
1161
+ return usageOutcome('CLI_USAGE_INVALID', `use takes a sibling slug and either one checkout directory or --clear: ${shape}`);
1162
+ }
1163
+ if (!values.config)
1164
+ return missingConfig();
1165
+ return { handler, input: { configPath: values.config, slug, clear: values.clear === true,
1166
+ ...(checkout === undefined ? {} : { checkout: resolve(checkout) }) } };
1167
+ }
1168
+ if (handler === 'pull') {
1169
+ const values = parsed.values;
1170
+ if (parsed.positionals.length > 0)
1171
+ return usageOutcome('CLI_USAGE_INVALID', 'pull accepts named inputs only');
1172
+ if (!values.config)
1173
+ return missingConfig();
1174
+ return { handler, input: { configPath: values.config } };
1175
+ }
1039
1176
  if (handler === 'tool') {
1040
1177
  const values = parsed.values;
1041
1178
  const [operation, tool, ...extra] = parsed.positionals;
@@ -1089,7 +1226,7 @@ async function invokeHandler(request, handlers, operators, options, io) {
1089
1226
  async function dispatchHandler(request, handlers, operators, options, io) {
1090
1227
  const unavailable = () => usageOutcome('CLI_INTERNAL_ERROR', `the "${request.handler}" command is unavailable`, request.handler);
1091
1228
  if (isWorkHandler(request.handler) && 'request' in request.input) {
1092
- if (request.handler === 'work-accept' || request.handler === 'work-answer' || request.handler === 'work-signoff') {
1229
+ if (request.handler === 'work-accept' || request.handler === 'work-answer' || request.handler === 'work-signoff' || request.handler === 'work-acknowledge') {
1093
1230
  const opened = await operators.beginOperatorWorkApplication?.(request.input);
1094
1231
  if (!opened)
1095
1232
  return unavailable();
@@ -1170,6 +1307,10 @@ async function dispatchHandler(request, handlers, operators, options, io) {
1170
1307
  return runSyncOperatorCommand(request.input, options, io, handlers.sync ?? (async () => unavailable()), operators.loadPolicy ?? loadOperatorPolicy);
1171
1308
  if (request.handler === 'check')
1172
1309
  return handlers.check?.(request.input) ?? unavailable();
1310
+ if (request.handler === 'docs-check')
1311
+ return handlers['docs-check']?.(request.input) ?? unavailable();
1312
+ if (request.handler === 'confirm')
1313
+ return handlers.confirm?.(request.input) ?? unavailable();
1173
1314
  if (request.handler === 'selftest') {
1174
1315
  const selftest = handlers.selftest ?? (async () => unavailable());
1175
1316
  return 'writeProbeSpecificationsPath' in request.input ? selftest(request.input)
@@ -1201,12 +1342,49 @@ async function dispatchHandler(request, handlers, operators, options, io) {
1201
1342
  return handlers.doctor?.(request.input) ?? unavailable();
1202
1343
  if (request.handler === 'logs')
1203
1344
  return handlers.logs?.(request.input) ?? unavailable();
1345
+ if (request.handler === 'ps')
1346
+ return handlers.ps?.(request.input) ?? unavailable();
1347
+ if (request.handler === 'restart')
1348
+ return handlers.restart?.(request.input) ?? unavailable();
1349
+ if (request.handler === 'rebuild')
1350
+ return handlers.rebuild?.(request.input) ?? unavailable();
1351
+ if (request.handler === 'exec')
1352
+ return handlers.exec?.(request.input) ?? unavailable();
1204
1353
  if (request.handler === 'runtime')
1205
1354
  return handlers.runtime?.(request.input) ?? unavailable();
1355
+ if (request.handler === 'stacks') {
1356
+ if (request.input.operation === 'list')
1357
+ return handlers.stacks?.(request.input) ?? unavailable();
1358
+ return runStacksPruneOperatorApplication({ configPath: request.input.configPath }, {
1359
+ ...options,
1360
+ terminal: io.confirmStacksPrune !== undefined || io.stdin?.isTTY === true,
1361
+ }, {
1362
+ policy: operators.loadPolicy ?? loadOperatorPolicy,
1363
+ begin: operators.beginStacksPrune ?? (async () => unavailable()),
1364
+ confirm: async (preview) => {
1365
+ if (io.confirmStacksPrune)
1366
+ return io.confirmStacksPrune(preview);
1367
+ if (!io.stdin)
1368
+ return undefined;
1369
+ io.stderr.write(renderHumanValue(preview.result));
1370
+ const prompt = createInterface({ input: io.stdin, output: io.stderr, terminal: true });
1371
+ try {
1372
+ return (await prompt.question('Type the exact prune digest to remove these stacks: ')).trim();
1373
+ }
1374
+ finally {
1375
+ prompt.close();
1376
+ }
1377
+ },
1378
+ });
1379
+ }
1206
1380
  if (request.handler === 'sibling')
1207
1381
  return handlers.sibling?.(request.input) ?? unavailable();
1208
1382
  if (request.handler === 'tool')
1209
1383
  return handlers.tool?.(request.input) ?? unavailable();
1384
+ if (request.handler === 'use')
1385
+ return handlers.use?.(request.input) ?? unavailable();
1386
+ if (request.handler === 'pull')
1387
+ return handlers.pull?.(request.input) ?? unavailable();
1210
1388
  return unavailable();
1211
1389
  }
1212
1390
  function operatorRefusal(message) {
@@ -1237,6 +1415,7 @@ function defaultOperatorHandlers(runtime) {
1237
1415
  return { digest: outcome.continuation.digest, prompt: outcome.continuation.prompt,
1238
1416
  decide: async (answer) => workCommandOutcome(await outcome.continuation.decide(answer), runtime.env ?? process.env) };
1239
1417
  },
1418
+ beginStacksPrune: (input) => beginStacksPrune({ configPath: resolve(input.configPath), env: runtime.env ?? process.env }, input.scope, { execute: executeProbe }),
1240
1419
  beginOperatorReset: async (input) => {
1241
1420
  if (sessionId === null)
1242
1421
  return operatorRefusal('COCKPIT_SESSION_ID must be a canonical UUID');
@@ -1264,7 +1443,7 @@ export async function runCli(args, io = process, handlerOverrides = {}, runtime
1264
1443
  exitCode, resolvedConfigPath });
1265
1444
  io.stdout.write(renderJsonResponse({ ...outcome, invocation }, outcome.fixContext));
1266
1445
  }
1267
- return verb === 'session' && Number.isInteger(status) && status > 0 && status < 256 ? status : exitCode;
1446
+ return (verb === 'session' || verb === 'exec') && Number.isInteger(status) && status > 0 && status < 256 ? status : exitCode;
1268
1447
  };
1269
1448
  // One refusal here instead of per-command checks: process ownership, filesystem publication and
1270
1449
  // the shell guards assume POSIX semantics, so a native Windows run could fail part-way through a