@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
@@ -60,6 +60,22 @@ export declare const WorkRecord: z.ZodObject<{
60
60
  from: z.ZodString;
61
61
  to: z.ZodString;
62
62
  weakened: z.ZodArray<z.ZodString>;
63
+ review: z.ZodArray<z.ZodObject<{
64
+ field: z.ZodString;
65
+ before: z.ZodString;
66
+ after: z.ZodString;
67
+ }, z.core.$strict>>;
68
+ }, z.core.$strict>>;
69
+ acknowledgements: z.ZodArray<z.ZodObject<{
70
+ at: z.ZodString;
71
+ by: z.ZodEnum<{
72
+ agent: "agent";
73
+ ci: "ci";
74
+ person: "person";
75
+ }>;
76
+ digest: z.ZodString;
77
+ findings: z.ZodArray<z.ZodString>;
78
+ note: z.ZodString;
63
79
  }, z.core.$strict>>;
64
80
  attempts: z.ZodNumber;
65
81
  checks: z.ZodOptional<z.ZodObject<{
@@ -105,6 +121,10 @@ export declare const WorkRecord: z.ZodObject<{
105
121
  }, z.core.$strict>>;
106
122
  note: z.ZodOptional<z.ZodString>;
107
123
  }, z.core.$strict>>;
124
+ amendmentsFailedReview: z.ZodOptional<z.ZodObject<{
125
+ at: z.ZodString;
126
+ note: z.ZodString;
127
+ }, z.core.$strict>>;
108
128
  notes: z.ZodArray<z.ZodObject<{
109
129
  at: z.ZodString;
110
130
  by: z.ZodEnum<{
@@ -114,11 +134,15 @@ export declare const WorkRecord: z.ZodObject<{
114
134
  }>;
115
135
  kind: z.ZodEnum<{
116
136
  answer: "answer";
137
+ decision: "decision";
117
138
  note: "note";
118
139
  question: "question";
140
+ resolution: "resolution";
119
141
  }>;
120
142
  text: z.ZodString;
121
143
  answers: z.ZodOptional<z.ZodNumber>;
144
+ resolves: z.ZodOptional<z.ZodNumber>;
145
+ record: z.ZodOptional<z.ZodString>;
122
146
  }, z.core.$strict>>;
123
147
  closure: z.ZodOptional<z.ZodObject<{
124
148
  at: z.ZodString;
@@ -136,6 +160,11 @@ export declare const WorkRecord: z.ZodObject<{
136
160
  }>;
137
161
  note: z.ZodOptional<z.ZodString>;
138
162
  }, z.core.$strict>>;
163
+ recoveries: z.ZodOptional<z.ZodArray<z.ZodObject<{
164
+ at: z.ZodString;
165
+ holder: z.ZodString;
166
+ checked: z.ZodArray<z.ZodString>;
167
+ }, z.core.$strict>>>;
139
168
  }, z.core.$strict>;
140
169
  export type WorkRecord = z.infer<typeof WorkRecord>;
141
170
  /** Keeps the end of a command's output, where a runner prints its summary, within the record's bound. */
@@ -148,11 +177,31 @@ export interface WorkStore {
148
177
  record?: WorkRecord;
149
178
  value: T;
150
179
  }>) => Promise<T>;
151
- readonly recover: (id: string, apply: boolean) => Promise<Readonly<{
152
- id: string;
153
- lock: 'none' | 'live' | 'unknown' | 'dead' | 'removed';
154
- holder?: string;
155
- }>>;
180
+ /** Names a lock's holder and, with `apply`, removes the lock once the holder is shown gone, recording what was checked. */
181
+ readonly recover: (id: string, apply: boolean, containers: () => Promise<ContainerEvidence>) => Promise<LockRecovery>;
156
182
  }
183
+ /** The containers cockpit labels on this machine's Docker daemon, which a process probe cannot look inside. */
184
+ export type ContainerEvidence = Readonly<{
185
+ state: 'listed';
186
+ context: string;
187
+ containers: readonly Readonly<{
188
+ id: string;
189
+ name: string;
190
+ running: boolean;
191
+ }>[];
192
+ }> | Readonly<{
193
+ state: 'no-docker';
194
+ reason: string;
195
+ }> | Readonly<{
196
+ state: 'unavailable';
197
+ reason: string;
198
+ }>;
199
+ type LockRecovery = Readonly<{
200
+ id: string;
201
+ lock: 'none' | 'live' | 'unknown' | 'dead' | 'removed';
202
+ holder?: string;
203
+ checked?: readonly string[];
204
+ recorded?: boolean;
205
+ }>;
157
206
  export declare function createWorkStore(root: string): WorkStore;
158
207
  export {};
@@ -5,7 +5,9 @@
5
5
  // an agent wrote there would read as cockpit's own. A write holds the order's lock file, which a
6
6
  // crashed writer leaves behind until `work recover --apply` removes it.
7
7
  import { randomUUID } from 'node:crypto';
8
- import { lstat, mkdir, open, readFile, readdir, rename, rm } from 'node:fs/promises';
8
+ import { existsSync } from 'node:fs';
9
+ import { link, lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
10
+ import { hostname } from 'node:os';
9
11
  import { join } from 'node:path';
10
12
  import { z } from 'zod';
11
13
  import { isProcessGone, observeProcessStart, processHost } from '../environment/claims.js';
@@ -46,24 +48,62 @@ export const WorkRecord = z.object({
46
48
  sources: z.record(z.string(), z.string().length(64)),
47
49
  base: z.record(z.string(), z.string().regex(/^[a-f0-9]{40,64}$/)),
48
50
  }).strict(),
49
- amendments: z.array(z.object({ at, by, from: z.string().length(64), to: z.string().length(64), weakened: z.array(z.string()).max(64) }).strict()).max(256),
51
+ // `weakened` names what the amendment removed; `review` each change a model, or a person where none grades, judges
52
+ // against the bar, with its text before and after. An agent's amendment is judged at the next grade.
53
+ amendments: z.array(z.object({ at, by, from: z.string().length(64), to: z.string().length(64), weakened: z.array(z.string()).max(64),
54
+ review: z.array(z.object({ field: z.string().max(64), before: z.string().max(131_072), after: z.string().max(131_072) }).strict()).max(128) }).strict()).max(256),
55
+ // A person's (or, where [work.gates] allows, an agent's or CI's) acknowledgement that the change weakened tests:
56
+ // it covers exactly the findings its digest names, so a later weakening needs its own.
57
+ acknowledgements: z.array(z.object({ at, by, digest: z.string().length(64), findings: z.array(z.string().max(1_100)).max(256), note }).strict()).max(256),
50
58
  attempts: z.number().int().min(0),
51
59
  checks: z.object({ at, digest: z.string().length(64), results: z.array(CheckResult).max(64) }).strict().optional(),
52
60
  grade: Grade.optional(),
53
- notes: z.array(z.object({ at, by: z.enum(['agent', 'person', 'ci']), kind: z.enum(['note', 'question', 'answer']), text: note,
54
- answers: z.number().int().min(1).optional() }).strict()).max(1_024),
61
+ // A grade found the agent's amendments lowered the bar. It outlives the grade a later check or grade
62
+ // replaces, and only an amendment a person or CI accepts clears it, so the next amendment stays theirs.
63
+ amendmentsFailedReview: z.object({ at, note: z.string().max(4_096) }).strict().optional(),
64
+ // A decision is resolved by a later `resolution` note naming it in `resolves`: with `record`, the decision
65
+ // record it is written in; without, `text` says why it made no lasting decision. The latest resolution counts.
66
+ notes: z.array(z.object({ at, by: z.enum(['agent', 'person', 'ci']), kind: z.enum(['note', 'question', 'answer', 'decision', 'resolution']), text: note,
67
+ answers: z.number().int().min(1).optional(), resolves: z.number().int().min(1).optional(), record: z.string().max(1_100).optional() }).strict()).max(1_024),
55
68
  closure: z.object({ at, by, how: z.enum(['graded', 'checks-passed', 'self-reported', 'partial', 'abandoned']), note: note.optional() }).strict().optional(),
69
+ // Each lock `work recover --apply` removed: the holder it named and what was checked to call it gone.
70
+ recoveries: z.array(z.object({ at, holder: z.string().max(1_024), checked: z.array(z.string().max(1_024)).max(16) }).strict()).max(256).optional(),
56
71
  }).strict();
57
72
  /** Keeps the end of a command's output, where a runner prints its summary, within the record's bound. */
58
73
  export function outputTail(value) {
59
74
  const bytes = Buffer.from(value);
60
75
  return bytes.byteLength <= OUTPUT_TAIL_BYTES ? value : `[...]${bytes.subarray(bytes.byteLength - OUTPUT_TAIL_BYTES).toString('utf8')}`;
61
76
  }
62
- // A lock names its holder as pid@start, so a reused pid never keeps a dead writer's lock alive.
77
+ // A lock names its writer: pid and start, so a reused pid never keeps a dead writer's lock alive, and
78
+ // the host and whether it ran in a container, because a pid means nothing outside the process table
79
+ // it came from. Docker names a container's host after the container, unless it shares the machine's network.
80
+ const LockHolder = z.object({ pid: z.number().int().min(1), start: z.string().max(256).optional(), host: z.string().min(1).max(255), container: z.boolean() }).strict();
81
+ const inContainer = () => existsSync('/.dockerenv') || existsSync('/run/.containerenv');
63
82
  const holder = async () => {
64
83
  const observed = await observeProcessStart(process.pid, processHost(executeProbe));
65
- return observed.state === 'present' ? `${process.pid}@${observed.start}` : `${process.pid}`;
84
+ return { pid: process.pid, ...observed.state === 'present' ? { start: observed.start } : {}, host: hostname(), container: inContainer() };
66
85
  };
86
+ // Whether the holder is shown running or gone, looking first where a pid probe cannot; anything unproven is unknown.
87
+ async function judgeHolder(found, containers) {
88
+ const here = hostname();
89
+ const listed = containers.state === 'listed' ? containers.containers : [];
90
+ const checked = [`the lock was written by process ${found.pid} on host ${found.host}${found.container ? ' inside a container' : ''}`, `this machine is host ${here}`,
91
+ (containers.state === 'listed' ? `Docker context ${containers.context} lists ${listed.length} cockpit-labelled container(s)${listed.length
92
+ ? `: ${listed.map(row => `${row.name} (${row.running ? 'running' : 'stopped'})`).join(', ')}` : ''}`
93
+ : containers.state === 'no-docker' ? `no Docker client is installed (${containers.reason}), so no container on this machine runs cockpit`
94
+ : `Docker could not be read: ${containers.reason}`).slice(0, 1_024)];
95
+ if (containers.state === 'unavailable')
96
+ return { lock: 'unknown', checked };
97
+ if (found.host !== here || found.container) {
98
+ const writer = /^[a-f0-9]{12}$/.test(found.host) ? listed.find(row => row.id.startsWith(found.host)) : undefined;
99
+ if (!writer)
100
+ return { lock: 'unknown', checked: [...checked, 'the writer ran on another host or in a container this machine does not list, whose processes this machine cannot see'] };
101
+ return { lock: writer.running ? 'live' : 'dead', checked: [...checked, `container ${writer.name}, which wrote it, is ${writer.running ? 'running' : 'stopped, so none of its processes runs'}`] };
102
+ }
103
+ const observed = await observeProcessStart(found.pid, processHost(executeProbe));
104
+ const lock = isProcessGone(found.start, observed) ? 'dead' : observed.state === 'unknown' ? 'unknown' : 'live';
105
+ return { lock, checked: [...checked, `process ${found.pid} on this machine is ${lock === 'dead' ? 'gone' : observed.state === 'unknown' ? `not observable (${observed.reason})` : 'running'}`] };
106
+ }
67
107
  export function createWorkStore(root) {
68
108
  const directory = join(root, WORK_RECORD_DIRECTORY);
69
109
  const pathOf = (id, suffix) => {
@@ -103,21 +143,21 @@ export function createWorkStore(root) {
103
143
  const lock = async (id) => {
104
144
  await mkdir(directory, { recursive: true });
105
145
  const path = pathOf(id, '.lock');
106
- const recorded = await holder();
146
+ // The holder is written before the lock appears, by linking a finished file into place, so a
147
+ // crash never leaves a lock that names nobody and so could never be shown gone.
148
+ const staged = `${path}.${randomUUID()}.tmp`;
149
+ await writeFile(staged, `${JSON.stringify(await holder())}\n`, { flag: 'wx', mode: 0o644 });
107
150
  try {
108
- const handle = await open(path, 'wx', 0o644);
109
- try {
110
- await handle.writeFile(`${recorded}\n`);
111
- }
112
- finally {
113
- await handle.close();
114
- }
151
+ await link(staged, path);
115
152
  }
116
153
  catch (error) {
117
154
  if (!hasErrorCode(error, 'EEXIST'))
118
155
  throw error;
119
156
  throw new RefusalError(`work order ${id} is locked by another cockpit command (${WORK_RECORD_DIRECTORY}/${id}.lock); wait for it, or if it crashed run npx --no cockpit work recover --config <your cockpit.toml> --work-order ${id} --apply`);
120
157
  }
158
+ finally {
159
+ await rm(staged, { force: true });
160
+ }
121
161
  return async () => { await rm(path, { force: true }); };
122
162
  };
123
163
  const write = async (record) => {
@@ -170,7 +210,7 @@ export function createWorkStore(root) {
170
210
  await release();
171
211
  }
172
212
  },
173
- recover: async (id, apply) => {
213
+ recover: async (id, apply, containers) => {
174
214
  const path = pathOf(id, '.lock');
175
215
  let text;
176
216
  try {
@@ -181,16 +221,70 @@ export function createWorkStore(root) {
181
221
  return { id, lock: 'none' };
182
222
  throw error;
183
223
  }
184
- const recorded = text.trim();
185
- const [pidText, start] = recorded.split('@', 2);
186
- const observed = await observeProcessStart(Number(pidText), processHost(executeProbe));
224
+ let parsed;
225
+ try {
226
+ parsed = JSON.parse(text);
227
+ }
228
+ catch (error) {
229
+ if (!(error instanceof SyntaxError))
230
+ throw error;
231
+ }
232
+ const found = LockHolder.safeParse(parsed);
233
+ const shown = text.trim().slice(0, 1_024);
234
+ if (!found.success)
235
+ return { id, lock: 'unknown', holder: shown, checked: ['the lock does not name its holder the way cockpit writes one'] };
187
236
  // A lock whose writer is still running, or cannot be shown gone, is never removed: the write it guards may be in progress.
188
- if (!isProcessGone(start, observed))
189
- return { id, lock: observed.state === 'unknown' ? 'unknown' : 'live', holder: recorded };
190
- if (!apply)
191
- return { id, lock: 'dead', holder: recorded };
192
- await rm(path, { force: true });
193
- return { id, lock: 'removed', holder: recorded };
237
+ const judged = await judgeHolder(found.data, await containers());
238
+ if (judged.lock !== 'dead' || !apply)
239
+ return { id, lock: judged.lock, holder: shown, checked: judged.checked };
240
+ // The lock is taken over rather than deleted, so no other writer slips in before the recovery is recorded.
241
+ // Two recoveries that both judged it dead must not both replace it and then each delete the other's: each
242
+ // links its holder at the takeover name, which only one can create, and only that one replaces the lock.
243
+ const ours = `${path}.${randomUUID()}.tmp`;
244
+ const takeover = `${path}.takeover`;
245
+ await writeFile(ours, `${JSON.stringify(await holder())}\n`, { flag: 'wx', mode: 0o644 });
246
+ try {
247
+ try {
248
+ await link(ours, takeover);
249
+ }
250
+ catch (error) {
251
+ if (!hasErrorCode(error, 'EEXIST'))
252
+ throw error;
253
+ throw new RefusalError(`another work recover is taking over the lock on ${id}; rerun work recover once it finishes, `
254
+ + `or if none is running, a person removes ${WORK_RECORD_DIRECTORY}/${id}.lock.takeover, which a crashed recovery left`);
255
+ }
256
+ let replaced = false;
257
+ try {
258
+ let current;
259
+ try {
260
+ current = await readFile(path, 'utf8');
261
+ }
262
+ catch (error) {
263
+ if (!hasErrorCode(error, 'ENOENT'))
264
+ throw error;
265
+ }
266
+ if (current !== text)
267
+ throw new RefusalError(`the lock on ${id} changed while it was being recovered; rerun work recover`);
268
+ await rename(takeover, path);
269
+ replaced = true;
270
+ }
271
+ finally {
272
+ if (!replaced)
273
+ await rm(takeover, { force: true });
274
+ }
275
+ }
276
+ finally {
277
+ await rm(ours, { force: true });
278
+ }
279
+ try {
280
+ const record = await read(id);
281
+ if (record)
282
+ await write({ ...record, recoveries: [...record.recoveries ?? [], { at: new Date().toISOString(), holder: shown, checked: [...judged.checked] }] });
283
+ return { id, lock: 'removed', holder: shown, checked: judged.checked, recorded: record !== undefined };
284
+ }
285
+ finally {
286
+ await rm(path, { force: true });
287
+ }
194
288
  },
195
289
  };
196
290
  return store;
package/docs/README.md CHANGED
@@ -22,13 +22,16 @@ contributor pages live separately under docs/contributing/.
22
22
  ## Guides
23
23
 
24
24
  - [Joining a team's cockpit](joining-a-team.md): install and verify a cockpit from a fresh clone.
25
- - [Environment lifecycle](environment.md): setup, bring-up, health, logs, reset and seeding.
25
+ - [Environment lifecycle](environment.md): setup, bring-up, health, logs, reset and seeding;
26
+ worktrees, Compose profiles, one-service commands and pruning stacks across the machine.
26
27
  - [Compose patterns](compose-patterns.md): vendor stubs, TLS and data restore in plain Compose.
27
28
  - [Harness surfaces](harness-surfaces.md): what `sync` writes for each agent tool, and overlays.
28
29
  - [Extending cockpit](extending.md): supported extension points, what an upgrade replaces and the build-beside pattern.
29
30
  - [Safety and autonomy](safety-and-autonomy.md): what guards allow, ask about and deny.
30
- - [Work and browser workflows](work-and-browser.md): work orders, grading, browser checks and
31
- test tiers.
31
+ - [Work and browser workflows](work-and-browser.md): work orders, grading, test protection,
32
+ decisions, browser checks and test tiers.
33
+ - [Living documents and decision records](docs-check.md): find documents that cite removed paths
34
+ or make targets, and decision records whose numbers, index or statuses disagree.
32
35
 
33
36
  ## Reference
34
37
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  <!-- Generated by `npm run references`; edit its sources, not this file. -->
4
4
 
5
- <!-- Source manifest: 94a3b5d0068e28fa3dc3ce7751dd8e3dc2d35d7a5e067980a8b09ff479f3ea7a -->
5
+ <!-- Source manifest: 7db847121f6f4cf7fc6608625e755acc97326e8320ea425b807d7de8a283f70c -->
6
6
 
7
7
  This generated page is for users checking what cockpit actually enforces in each agent tool. Each cell holds one of four states:
8
8
 
@@ -15,15 +15,15 @@ Only a dated run of the real tool changes a state; writing a file or passing coc
15
15
 
16
16
  Written says whether `sync` writes the tool's files for a cockpit.toml that lists no tools: the tools whose guard firing is measured are the default `[surfaces] tools`, and any other tool is written only once listed (`npx --no cockpit tool add <tool> --config <your cockpit.toml>`).
17
17
 
18
- Guard firing is the generated hook refusing a protected operation. Instruction loading is the tool reading the guidance cockpit writes for it, including the guides of sibling repositories. Skills are the tool loading the skills cockpit renders: `cockpit-setup`, `cockpit-explain`, `cockpit-fix` and `cockpit-work`. The session-start check is the tool running `cockpit check` as a session starts and showing its drift report to the agent.
18
+ Guard firing is the generated hook refusing a protected operation. Instruction loading is the tool reading the guidance cockpit writes for it, including the guides of sibling repositories. Skills are the tool loading the skills cockpit renders: `cockpit-setup`, `cockpit-explain`, `cockpit-fix` and `cockpit-work`. The session-start check is the tool running `cockpit check` as a session starts and showing its drift report to the agent. Remote MCP servers are the tool connecting to an approved streamable HTTP server with the credential cockpit spells for it: an environment reference or a headers helper. Worktree grants are the tool letting the agent write a checkout `cockpit use` serves from outside the declared one, through a machine-local file cockpit writes.
19
19
 
20
- | Tool | Written | Guard firing | Instruction loading | Skills | Session-start check |
21
- | --- | --- | --- | --- | --- | --- |
22
- | Claude Code (`claudecode`) | by default | enforced | instructed | instructed | instructed |
23
- | Codex (`codexcli`) | by default | enforced (needs a person) | instructed | instructed | instructed (needs a person) |
24
- | OpenCode (`opencode`) | by default | enforced | instructed | instructed | unavailable |
25
- | Cursor (`cursor`) | when listed | unavailable | generated | generated | unavailable |
26
- | Grok Build (`grokcli`) | when listed | unavailable | generated | generated | unavailable |
20
+ | Tool | Written | Guard firing | Instruction loading | Skills | Session-start check | Remote MCP servers | Worktree grants |
21
+ | --- | --- | --- | --- | --- | --- | --- | --- |
22
+ | Claude Code (`claudecode`) | by default | enforced | instructed | instructed | instructed | instructed (needs a person) | enforced (needs a person) |
23
+ | Codex (`codexcli`) | by default | enforced (needs a person) | instructed | instructed | instructed (needs a person) | instructed | unavailable |
24
+ | OpenCode (`opencode`) | by default | enforced | instructed | instructed | unavailable | instructed | enforced |
25
+ | Cursor (`cursor`) | when listed | unavailable | generated | generated | unavailable | unavailable | unavailable |
26
+ | Grok Build (`grokcli`) | when listed | unavailable | generated | generated | unavailable | unavailable | unavailable |
27
27
 
28
28
  ## Measurements
29
29
 
@@ -37,6 +37,12 @@ Each line gives the date and tool version of the run and what it showed.
37
37
  - OpenCode, instruction loading (2026-11-03, version 1.19.5): a sibling guide copied into the root AGENTS.md loaded, as did a guide named through a link in the opencode.jsonc instructions list, while the undeclared sibling's guide and the decoy did not.
38
38
  - Claude Code, session-start check (2026-11-03, version 2.1.317): a SessionStart hook in .claude/settings.json ran under claude -p and the agent quoted its stdout back, while a hook under a misspelled event name never ran and Claude Code named it as ignored.
39
39
  - Codex, session-start check (2026-11-03, version 0.166.0): only after a person trusts the hook, as for the guard: a trusted SessionStart hook in .codex/hooks.json ran under codex exec and the agent quoted its stdout back, while a hook under a misspelled event name never ran.
40
+ - Claude Code, remote MCP servers (2026-11-04, version 2.1.318): from a project .mcp.json, Claude Code sent the Authorization header a ${NAME} reference named while a $NAME control went out literally; it ran a headersHelper given with --mcp-config, but refused to run one from the project file until a person trusts the workspace, and a misspelled headerHelper sent nothing.
41
+ - Codex, remote MCP servers (2026-11-04, version 0.166.0): from a project .codex/config.toml in a directory Codex had not been told to trust, codex exec sent the bearer_token_env_var and env_http_headers values and the http_headers_helper output, named a misspelled http_header_helper as ignored, and sent a ${NAME} written in http_headers literally.
42
+ - OpenCode, remote MCP servers (2026-11-04, version 1.19.5): opencode mcp list sent headers expanded from {env:NAME} in a project opencode.jsonc while a ${NAME} control went out literally; its remote server schema has no headers helper, so a server that declares one is not written for it.
43
+ - Claude Code, worktree grants (2026-11-04, version 2.1.318): a directory in .claude/settings.local.json permissions.additionalDirectories was written and the control refused, and a malformed twin granted nothing; measured in an untrusted workspace, so whether it combines with the committed list once a person trusts the workspace is unmeasured.
44
+ - Codex, worktree grants (2026-11-04, version 0.166.0): Codex reads no machine-local project file; --add-dir at start made the worktree writable and without it the write was refused, so cockpit use prints that flag and writes nothing.
45
+ - OpenCode, worktree grants (2026-11-04, version 1.19.5): the machine-local grant file holding the worktree path allowed the write and the control was denied, and a pattern naming a missing grant file stopped OpenCode at start, naming the file.
40
46
 
41
47
  ## Without a measurement
42
48
 
@@ -47,4 +53,6 @@ Each line gives the date and tool version of the run and what it showed.
47
53
  - Skills in Cursor (`.cursor/skills`) and Grok Build (`.grok/skills`): cockpit writes the files, but no run of the tool beside a broken control has shown that it loads them.
48
54
  - Session-start check in OpenCode: OpenCode 1.19.5 delivers session.created to a plugin, but no run has shown a plugin putting text in the agent's context, so cockpit generates no session-start check.
49
55
  - Session-start check in Cursor and Grok Build: cockpit generates no hook for this tool.
56
+ - Remote MCP servers in Cursor and Grok Build: cockpit writes no remote MCP server for this tool: its remote transport and header spelling have not been checked.
57
+ - Worktree grants in Cursor and Grok Build: the tool was not installed where this was measured, so cockpit use writes nothing for it.
50
58
  - Browser native transports (the playwright MCP tools and the pinned Playwright CLI) in Claude Code, Codex and OpenCode: cockpit declares them and the browser skill says each still needs a person's paired call beside a broken control; no cockpit command runs or records that call, so the browser commands drive the browser through cockpit's own project client instead.
@@ -53,6 +53,41 @@ Reads `cockpit.toml` from `--config`.
53
53
  | 2 | failure |
54
54
  | 130 | cancelled |
55
55
 
56
+ ## cockpit use
57
+
58
+ Serve a sibling from another of its checkouts, such as a worktree.
59
+
60
+ Reads `cockpit.toml` from `--config`.
61
+
62
+ | Flag | Type | Default |
63
+ | --- | --- | --- |
64
+ | `--config` | string | unset |
65
+ | `--clear` | boolean | `false` |
66
+ | `--json` | boolean | `false` |
67
+
68
+ | Exit | Meaning |
69
+ | --- | --- |
70
+ | 0 | success |
71
+ | 2 | failure |
72
+ | 130 | cancelled |
73
+
74
+ ## cockpit pull
75
+
76
+ Fast-forward the cockpit and its siblings to their upstreams.
77
+
78
+ Reads `cockpit.toml` from `--config`.
79
+
80
+ | Flag | Type | Default |
81
+ | --- | --- | --- |
82
+ | `--config` | string | unset |
83
+ | `--json` | boolean | `false` |
84
+
85
+ | Exit | Meaning |
86
+ | --- | --- |
87
+ | 0 | success |
88
+ | 2 | failure |
89
+ | 130 | cancelled |
90
+
56
91
  ## cockpit sibling
57
92
 
58
93
  Add or remove a repository in cockpit.toml (sibling add, sibling remove).
@@ -96,6 +131,23 @@ Reads `cockpit.toml` from `--config`.
96
131
  | 2 | failure |
97
132
  | 130 | cancelled |
98
133
 
134
+ ## cockpit confirm
135
+
136
+ Confirm an init answer no person stated by running the observation that verifies it.
137
+
138
+ Reads `cockpit.toml` from `--config`.
139
+
140
+ | Flag | Type | Default |
141
+ | --- | --- | --- |
142
+ | `--config` | string | unset |
143
+ | `--json` | boolean | `false` |
144
+
145
+ | Exit | Meaning |
146
+ | --- | --- |
147
+ | 0 | success |
148
+ | 2 | failure |
149
+ | 130 | cancelled |
150
+
99
151
  ## cockpit registry-resolve
100
152
 
101
153
  Turn a validated shell registry into data shell scripts can source.
@@ -217,6 +269,7 @@ Reads `cockpit.toml` from `--config`.
217
269
  | --- | --- | --- |
218
270
  | `--config` | string | unset |
219
271
  | `--json` | boolean | `false` |
272
+ | `--profile` | string | unset |
220
273
 
221
274
  | Exit | Meaning |
222
275
  | --- | --- |
@@ -250,6 +303,7 @@ Reads `cockpit.toml` from `--config`.
250
303
  | Flag | Type | Default |
251
304
  | --- | --- | --- |
252
305
  | `--config` | string | unset |
306
+ | `--profile` | string | unset |
253
307
 
254
308
  | Exit | Meaning |
255
309
  | --- | --- |
@@ -278,6 +332,76 @@ Reads `cockpit.toml` from `--config`.
278
332
  | 2 | failure |
279
333
  | 130 | cancelled |
280
334
 
335
+ ## cockpit ps
336
+
337
+ Show the state and health of each service in this checkout's stack.
338
+
339
+ Reads `cockpit.toml` from `--config`.
340
+
341
+ | Flag | Type | Default |
342
+ | --- | --- | --- |
343
+ | `--config` | string | unset |
344
+ | `--json` | boolean | `false` |
345
+
346
+ | Exit | Meaning |
347
+ | --- | --- |
348
+ | 0 | success |
349
+ | 2 | failure |
350
+ | 130 | cancelled |
351
+
352
+ ## cockpit restart
353
+
354
+ Restart one service of this checkout's stack and wait until it is healthy.
355
+
356
+ Reads `cockpit.toml` from `--config`.
357
+
358
+ | Flag | Type | Default |
359
+ | --- | --- | --- |
360
+ | `--config` | string | unset |
361
+ | `--json` | boolean | `false` |
362
+ | `--service` | string | unset |
363
+
364
+ | Exit | Meaning |
365
+ | --- | --- |
366
+ | 0 | success |
367
+ | 2 | failure |
368
+ | 130 | cancelled |
369
+
370
+ ## cockpit rebuild
371
+
372
+ Rebuild one service's image, replace its container and wait until it is healthy.
373
+
374
+ Reads `cockpit.toml` from `--config`.
375
+
376
+ | Flag | Type | Default |
377
+ | --- | --- | --- |
378
+ | `--config` | string | unset |
379
+ | `--json` | boolean | `false` |
380
+ | `--service` | string | unset |
381
+
382
+ | Exit | Meaning |
383
+ | --- | --- |
384
+ | 0 | success |
385
+ | 2 | failure |
386
+ | 130 | cancelled |
387
+
388
+ ## cockpit exec
389
+
390
+ Run a command inside one service's container of this checkout's stack.
391
+
392
+ Reads `cockpit.toml` from `--config`.
393
+
394
+ | Flag | Type | Default |
395
+ | --- | --- | --- |
396
+ | `--config` | string | unset |
397
+ | `--service` | string | unset |
398
+
399
+ | Exit | Meaning |
400
+ | --- | --- |
401
+ | 0 | success |
402
+ | 2 | failure |
403
+ | 130 | cancelled |
404
+
281
405
  ## cockpit sleep
282
406
 
283
407
  Release this checkout and stop the stack if no other holder remains.
@@ -331,6 +455,25 @@ Reads `cockpit.toml` from `--config`.
331
455
  | 2 | failure |
332
456
  | 130 | cancelled |
333
457
 
458
+ ## cockpit stacks
459
+
460
+ List every stack on this machine, or preview and remove those whose checkout is gone (stacks prune).
461
+
462
+ Reads `cockpit.toml` from `--config`.
463
+
464
+ | Flag | Type | Default |
465
+ | --- | --- | --- |
466
+ | `--config` | string | unset |
467
+ | `--apply` | boolean | `false` |
468
+ | `--interactive` | boolean | `false` |
469
+ | `--json` | boolean | `false` |
470
+
471
+ | Exit | Meaning |
472
+ | --- | --- |
473
+ | 0 | success |
474
+ | 2 | failure |
475
+ | 130 | cancelled |
476
+
334
477
  ## cockpit doctor
335
478
 
336
479
  Report environment problems without changing anything.
@@ -425,7 +568,7 @@ Reads `cockpit.toml` from `--config`.
425
568
 
426
569
  ## cockpit work note
427
570
 
428
- Record a note on an order, or with --ask an open question that blocks it.
571
+ Record a note on an order, an open question with --ask, or a decision with --decision.
429
572
 
430
573
  Reads `cockpit.toml` from `--config`.
431
574
 
@@ -436,6 +579,31 @@ Reads `cockpit.toml` from `--config`.
436
579
  | `--work-order` | string | unset |
437
580
  | `--text` | string | unset |
438
581
  | `--ask` | boolean | `false` |
582
+ | `--decision` | boolean | `false` |
583
+
584
+ | Exit | Meaning |
585
+ | --- | --- |
586
+ | 0 | success |
587
+ | 2 | failure |
588
+ | 130 | cancelled |
589
+
590
+ ## cockpit work decide
591
+
592
+ Resolve a recorded decision: write or cite its decision record, or say it is not lasting.
593
+
594
+ Reads `cockpit.toml` from `--config`.
595
+
596
+ | Flag | Type | Default |
597
+ | --- | --- | --- |
598
+ | `--config` | string | unset |
599
+ | `--json` | boolean | `false` |
600
+ | `--work-order` | string | unset |
601
+ | `--decision` | string | unset |
602
+ | `--record` | boolean | `false` |
603
+ | `--record-file` | string | unset |
604
+ | `--supersedes` | string | unset |
605
+ | `--none` | boolean | `false` |
606
+ | `--reason` | string | unset |
439
607
 
440
608
  | Exit | Meaning |
441
609
  | --- | --- |
@@ -464,6 +632,26 @@ Reads `cockpit.toml` from `--config`.
464
632
  | 2 | failure |
465
633
  | 130 | cancelled |
466
634
 
635
+ ## cockpit work acknowledge
636
+
637
+ Acknowledge that an order weakened tests, as [work.gates] allows.
638
+
639
+ Reads `cockpit.toml` from `--config`.
640
+
641
+ | Flag | Type | Default |
642
+ | --- | --- | --- |
643
+ | `--config` | string | unset |
644
+ | `--json` | boolean | `false` |
645
+ | `--work-order` | string | unset |
646
+ | `--note` | string | unset |
647
+ | `--interactive` | boolean | `false` |
648
+
649
+ | Exit | Meaning |
650
+ | --- | --- |
651
+ | 0 | success |
652
+ | 2 | failure |
653
+ | 130 | cancelled |
654
+
467
655
  ## cockpit work grade
468
656
 
469
657
  Grade an accepted order the way [work] grading says.
@@ -573,6 +761,8 @@ Reads `cockpit.toml` from `--config`.
573
761
  | `--config` | string | unset |
574
762
  | `--json` | boolean | `false` |
575
763
  | `--role` | string | unset |
764
+ | `--route` | string | unset |
765
+ | `--fail-on-findings` | boolean | `false` |
576
766
 
577
767
  | Exit | Meaning |
578
768
  | --- | --- |
@@ -591,6 +781,24 @@ Reads `cockpit.toml` from `--config`.
591
781
  | `--config` | string | unset |
592
782
  | `--json` | boolean | `false` |
593
783
  | `--role` | string | unset |
784
+ | `--route` | string | unset |
785
+
786
+ | Exit | Meaning |
787
+ | --- | --- |
788
+ | 0 | success |
789
+ | 2 | failure |
790
+ | 130 | cancelled |
791
+
792
+ ## cockpit docs check
793
+
794
+ Report removed paths and make targets living docs still cite, and broken decision records.
795
+
796
+ Reads `cockpit.toml` from `--config`.
797
+
798
+ | Flag | Type | Default |
799
+ | --- | --- | --- |
800
+ | `--config` | string | unset |
801
+ | `--json` | boolean | `false` |
594
802
 
595
803
  | Exit | Meaning |
596
804
  | --- | --- |