@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
@@ -2,66 +2,20 @@
2
2
  // derived from the record and the current file, and partial and abandoned as closures. Each command
3
3
  // that passes a gate asks the autonomy policy who may pass it; a person gate returns a continuation
4
4
  // the CLI completes only with the digest a person types in a terminal. It also routes the browser and
5
- // test-gate operations that share the work composition.
6
- import { createHash } from 'node:crypto';
7
- import { posix } from 'node:path';
8
- import { z } from 'zod';
9
- import { decide, policyWeakening, runningInCi } from '../autonomy.js';
5
+ // test-gate operations that share the work composition. Decisions, grading and closure are owned by the
6
+ // application-* modules beside it, on the shared steps application-context.ts binds.
10
7
  import { executeBrowserCommand } from '../browser/commands.js';
11
8
  import { evaluateUiGate } from '../browser/ui-gate.js';
9
+ import { policyWeakening } from '../autonomy.js';
12
10
  import { errorCause, RefusalError } from '../environment/contract.js';
13
- import { runGate } from '../testing/gates.js';
14
- import { criteriaWeakening, scopeMatches, WORK_ORDER_ID, workOrderDigest } from './contract.js';
11
+ import { describeWeakening, testWeakeningDigest } from '../testing/weakening.js';
12
+ import { checksBindsField, contractAmendment, workOrderDigest } from './contract.js';
15
13
  import { isWorkApplicationComposition } from './composition.js';
16
- const id = z.string().regex(WORK_ORDER_ID);
17
- const note = z.string().trim().min(1).max(4_096).regex(/^[^\x00-\x08\x0b-\x1f\x7f]*$/);
18
- const relativeFile = z.string().max(4_096).regex(/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._@+\-/]+$/);
19
- const Request = z.discriminatedUnion('operation', [
20
- z.object({ operation: z.literal('work-status'), workOrderId: id.optional() }).strict(),
21
- z.object({ operation: z.literal('work-accept'), workOrderId: id }).strict(),
22
- z.object({ operation: z.literal('work-check'), workOrderId: id }).strict(),
23
- z.object({ operation: z.literal('work-note'), workOrderId: id, text: note, question: z.boolean() }).strict(),
24
- z.object({ operation: z.literal('work-answer'), workOrderId: id, question: z.number().int().min(1), text: note }).strict(),
25
- z.object({ operation: z.literal('work-grade'), workOrderId: id, verdict: z.enum(['met', 'unmet']).optional(), note: note.optional() }).strict(),
26
- z.object({ operation: z.literal('work-signoff'), workOrderId: id, close: z.enum(['done', 'partial', 'abandoned']), note: note.optional() }).strict(),
27
- z.object({ operation: z.literal('work-recover'), workOrderId: id, apply: z.boolean() }).strict(),
28
- z.object({ operation: z.literal('work-verify') }).strict(),
29
- z.object({ operation: z.literal('grade-criteria'), criteriaFile: relativeFile }).strict(),
30
- z.object({ operation: z.enum(['browser-drive', 'browser-screenshot']), role: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/).optional() }).strict(),
31
- z.object({ operation: z.literal('test-run'), gate: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/), mode: z.enum(['canonical', 'full', 'staged']),
32
- select: z.object({ kind: z.enum(['spec', 'file', 'test', 'route']), value: z.string().min(1).refine(value => Buffer.byteLength(value) <= 4_096 && !/[\x00-\x1f\x7f]/.test(value)) }).strict().optional() }).strict(),
33
- ]);
34
- export function captureWorkApplicationRequest(value) {
35
- const parsed = Request.safeParse(value);
36
- return parsed.success ? parsed.data : undefined;
37
- }
38
- const refusal = (operation, reason) => ({ kind: 'refusal', operation, reason });
39
- const complete = (operation, value) => ({ kind: 'complete', operation, value });
40
- const unsatisfied = (operation, value, reason) => ({ kind: 'unsatisfied', operation, value, reason });
41
- const now = () => new Date().toISOString();
42
- // Agent-written text is shown to a person with its control and bidirectional-override characters
43
- // replaced, so it can neither drive their terminal nor read in an order other than the one recorded.
44
- const shown = (value) => value.replace(/[\x00-\x09\x0b-\x1f\x7f-\x9f‪-‮⁦-⁩]/g, '�');
45
- const TERMINAL = new Set(['done', 'partial', 'abandoned']);
46
- const CLOSURE = Object.freeze({ independent: 'graded', checks: 'checks-passed', self: 'self-reported' });
47
- function displayState(row, attemptCap) {
48
- if (!row.record)
49
- return row.order ? 'draft' : 'invalid';
50
- if (TERMINAL.has(row.record.state))
51
- return row.record.state;
52
- if (!row.order || row.digest !== row.record.accepted.digest)
53
- return 'stale';
54
- if (openQuestions(row.record).length || row.record.attempts >= attemptCap)
55
- return 'blocked';
56
- return row.record.state;
57
- }
58
- function openQuestions(record) {
59
- return record.notes.flatMap((row, index) => row.kind === 'question' && !record.notes.some(answer => answer.answers === index + 1) ? [index + 1] : []);
60
- }
61
- const pendingCi = (record) => [
62
- ...record.accepted.by === 'ci' ? ['accept'] : [], ...record.amendments.filter(row => row.by === 'ci').map(() => 'amend'),
63
- ...record.notes.filter(row => row.kind === 'answer' && row.by === 'ci').map(() => 'answer'), ...record.closure?.by === 'ci' ? ['signoff'] : [],
64
- ];
14
+ import { captureWorkApplicationRequest, complete, displayState, now, openQuestions, refusal, TERMINAL, unsatisfied, workContext } from './application-context.js';
15
+ import { pendingCi, signoff, verify } from './application-closure.js';
16
+ import { decideOrder, DECISIONS_UNDECLARED, relatedDecisions, unresolvedDecisions } from './application-decisions.js';
17
+ import { gradeCriteriaFile, gradeOrder, testRun } from './application-grading.js';
18
+ export { captureWorkApplicationRequest } from './application-context.js';
65
19
  export async function runWorkApplication(request, composition) {
66
20
  const captured = captureWorkApplicationRequest(request);
67
21
  if (!captured)
@@ -72,21 +26,10 @@ export async function runWorkApplication(request, composition) {
72
26
  try {
73
27
  if (captured.operation === 'browser-drive' || captured.operation === 'browser-screenshot') {
74
28
  const kind = captured.operation === 'browser-screenshot' ? 'screenshot' : 'drive';
75
- const result = await executeBrowserCommand(await composition.browser(kind, captured.role));
76
- if (result.kind === 'refusal')
77
- return refusal(operation, result.reason);
78
- if (result.kind === 'complete') {
79
- const gate = evaluateUiGate({ activating: kind, receipts: [result.receipt] });
80
- if (gate.kind === 'refusal')
81
- return refusal(operation, gate.reason);
82
- }
83
- return complete(operation, result);
84
- }
85
- if (captured.operation === 'test-run') {
86
- const prepared = await composition.gate(captured);
87
- const run = await runGate(prepared.registry, prepared.request, prepared.dependencies);
88
- return run.satisfiesGate ? complete(operation, run) : unsatisfied(operation, run, `${run.verdict}: ${run.reasons.join('; ') || 'declared gate was not satisfied'}`);
29
+ return browserOutcome(captured.operation, await executeBrowserCommand(await composition.browser(kind, captured.role, captured.route)), captured.failOnFindings === true);
89
30
  }
31
+ if (captured.operation === 'test-run')
32
+ return await testRun(captured, composition);
90
33
  // A policy weakened in cockpit.toml since the last sync is not in force until a person accepts it there.
91
34
  const recorded = await composition.recordedPolicy();
92
35
  const weakened = recorded ? policyWeakening(recorded, composition.policy) : [];
@@ -100,87 +43,48 @@ export async function runWorkApplication(request, composition) {
100
43
  return refusal(operation, errorCause(error));
101
44
  }
102
45
  }
46
+ // Findings are evidence for the agent and the grader to weigh, so they fail the command only for a criterion that demands a clean route.
47
+ export function browserOutcome(operation, result, failOnFindings) {
48
+ if (result.kind === 'refusal')
49
+ return refusal(operation, result.reason);
50
+ const gate = evaluateUiGate({ activating: operation === 'browser-screenshot' ? 'screenshot' : 'drive', receipts: [result.receipt] });
51
+ if (gate.kind === 'refusal')
52
+ return refusal(operation, gate.reason);
53
+ if (failOnFindings && result.receipt.verdict === 'findings')
54
+ return unsatisfied(operation, result, browserFindingsReason(result.receipt));
55
+ return complete(operation, result);
56
+ }
57
+ function browserFindingsReason(receipt) {
58
+ return receipt.surfaces.flatMap(row => {
59
+ const found = row.diagnostics;
60
+ if (!found || row.verdict !== 'findings')
61
+ return [];
62
+ const parts = [[found.consoleErrors, 'console error'], [found.pageErrors, 'uncaught page error'], [found.failedRequests, 'failed request']]
63
+ .flatMap(([count, noun]) => count ? [`${count} ${noun}${count === 1 ? '' : 's'}`] : []);
64
+ return [`${row.name}: ${parts.join(', ')}`];
65
+ }).join('; ');
66
+ }
103
67
  async function work(request, composition) {
104
- const { policy, store } = composition;
105
- const operation = request.operation;
106
- const observe = async (orderId) => {
107
- const [parsed, record] = await Promise.all([composition.readOrder(orderId), store.read(orderId)]);
108
- if (!parsed.ok)
109
- return { id: orderId, problems: parsed.problems, ...(record ? { record } : {}) };
110
- // A source that cannot be read leaves this order without a digest, and so stale, not the whole listing refused.
111
- let sources;
112
- try {
113
- sources = await composition.sourceDigests(parsed.value);
114
- }
115
- catch (error) {
116
- if (!(error instanceof RefusalError))
117
- throw error;
118
- return { id: orderId, order: parsed.value, problems: [`a cited source cannot be read: ${error.message}`], ...(record ? { record } : {}) };
119
- }
120
- const base = record?.accepted.base ?? await composition.heads();
121
- return { id: orderId, order: parsed.value, problems: [], ...(record ? { record } : {}), digest: workOrderDigest(parsed.value, sources, base) };
122
- };
123
- const wip = async () => (await store.list()).filter(record => !TERMINAL.has(record.state)).length;
124
- // Who passed a gate: the agent; a person, after typing the digest of exactly what they were shown;
125
- // or CI, which locally records the step as awaiting the `work verify` job and passes it there.
126
- const gated = async (gate, decider, lines, verb, apply) => {
127
- const decision = decide(decider, composition.env);
128
- if (decision === 'proceed')
129
- return apply(decider === 'ci' ? 'ci' : 'agent');
130
- if (decision === 'ci-only')
131
- return apply('ci');
132
- const digest = createHash('sha256').update(JSON.stringify({ gate, lines })).digest('hex');
133
- let used = false;
134
- return { kind: 'human-needed', operation, continuation: { digest,
135
- prompt: [...lines.map(shown), `[work.gates] ${gate} = person. To ${verb} exactly this, type ${verb} ${digest}`].join('\n'),
136
- decide: async (answer) => {
137
- if (used)
138
- return refusal(operation, `this prompt was already answered; rerun work ${operation.slice('work-'.length)}`);
139
- used = true;
140
- return answer.trim() === `${verb} ${digest}` ? apply('person') : refusal(operation, `Nothing was recorded: the person did not type ${verb} ${digest}`);
141
- } } };
142
- };
143
- const current = async (orderId) => {
144
- const row = await observe(orderId);
145
- if (!row.record)
146
- throw new RefusalError(`${orderId} is not accepted yet; run npx --no cockpit work accept --config <your cockpit.toml> --work-order ${orderId}`);
147
- const state = displayState(row, composition.caps.attempts);
148
- if (TERMINAL.has(row.record.state))
149
- throw new RefusalError(`${orderId} is closed (${row.record.state})`);
150
- if (state === 'stale')
151
- throw new RefusalError(`${orderId} changed since it was accepted${row.problems.length ? ` and does not parse: ${row.problems.join('; ')}` : ''}; `
152
- + `run npx --no cockpit work accept --config <your cockpit.toml> --work-order ${orderId} to accept the change`);
153
- if (state === 'blocked')
154
- throw new RefusalError(openQuestions(row.record).length
155
- ? `${orderId} has open question ${openQuestions(row.record).join(', ')}; it is answered with work answer`
156
- : `${orderId} reached the attempt cap (${composition.caps.attempts}); record the root cause with work note, then a person closes it with work signoff --partial or --abandon, or amends the contract`);
157
- return { row, record: row.record, order: row.order };
158
- };
159
- const runChecks = async (order) => {
160
- const results = [];
161
- // Checks share the product's state, so they run in the order the contract lists them.
162
- for (const criterion of order.criteria)
163
- if (criterion.check)
164
- results.push(await composition.runCheck(criterion.id, criterion.check));
165
- for (const gate of order.gates)
166
- results.push(await composition.runCheck(`gate:${gate}`, { gate }));
167
- return results;
168
- };
68
+ const context = workContext(composition, request.operation);
69
+ const { operation, policy, store, observe, wip, gated, current, runChecks } = context;
169
70
  if (request.operation === 'work-status') {
170
71
  const ids = request.workOrderId ? [request.workOrderId] : [...new Set([...await composition.orderIds(), ...(await store.list()).map(row => row.id)])].sort();
171
72
  const rows = await Promise.all(ids.map(observe));
172
73
  const closed = new Set(rows.filter(row => row.record?.state === 'done').map(row => row.id));
173
74
  return complete(operation, {
174
75
  grading: policy.grading, gates: policy.gates, wip: { open: await wip(), cap: composition.caps.wip },
175
- orders: rows.map(row => ({
76
+ orders: await Promise.all(rows.map(async (row) => ({
176
77
  id: row.id, state: displayState(row, composition.caps.attempts),
177
78
  ...row.record?.closure ? { closedAs: row.record.closure.how, closedBy: row.record.closure.by } : {},
178
79
  ...row.record ? { attempts: row.record.attempts, acceptedBy: row.record.accepted.by, openQuestions: openQuestions(row.record),
80
+ acknowledgements: row.record.acknowledgements.map(({ by, note, findings }) => ({ by, note, findings })),
179
81
  awaitingCi: pendingCi(row.record), ...row.record.grade ? { grade: { mode: row.record.grade.mode, passed: row.record.grade.passed,
180
82
  criteria: row.record.grade.criteria, outOfScope: row.record.grade.outOfScope, ...row.record.grade.evaluator ? { evaluator: row.record.grade.evaluator } : {} } } : {} } : {},
181
83
  ...row.order ? { waitingOn: row.order.depends_on.filter(dependency => !closed.has(dependency)) } : {},
84
+ ...row.record && !TERMINAL.has(row.record.state) ? { openDecisions: await unresolvedDecisions(row.record, composition) } : {},
85
+ ...row.order && !TERMINAL.has(row.record?.state ?? 'accepted') ? await relatedDecisions(context, row.order) : {},
182
86
  ...row.problems.length ? { problems: row.problems } : {},
183
- })),
87
+ }))),
184
88
  });
185
89
  }
186
90
  if (request.operation === 'work-accept') {
@@ -200,8 +104,8 @@ async function work(request, composition) {
200
104
  if (existing)
201
105
  throw new RefusalError(`${order.id} was accepted while this prompt was open; rerun work status`);
202
106
  const record = { format: 'cockpit-work-order/v1', id: order.id, state: 'accepted',
203
- accepted: { at: now(), by, digest, order, sources, base }, amendments: [], attempts: 0, notes: [] };
204
- return { record, value: complete(operation, { id: order.id, state: 'accepted', by, digest, ...by === 'ci' ? { awaitingCi: ['accept'] } : {} }) };
107
+ accepted: { at: now(), by, digest, order, sources, base }, amendments: [], acknowledgements: [], attempts: 0, notes: [] };
108
+ return { record, value: complete(operation, { id: order.id, state: 'accepted', by, digest, ...by === 'ci' ? { awaitingCi: ['accept'] } : {}, ...await relatedDecisions(context, order) }) };
205
109
  }));
206
110
  }
207
111
  const record = row.record;
@@ -209,19 +113,27 @@ async function work(request, composition) {
209
113
  return refusal(operation, `${order.id} is closed (${record.state}); write a new order for further work`);
210
114
  if (row.digest === record.accepted.digest)
211
115
  return refusal(operation, `${order.id} is already accepted and unchanged since`);
212
- const weakened = criteriaWeakening(record.accepted.order, order);
213
- // Removing or rewriting an accepted criterion moves the bar the work is held to, which an agent never certifies.
214
- const decider = weakened.length && policy.gates.amend === 'agent' ? 'person' : policy.gates.amend;
116
+ const { removed: weakened, review } = contractAmendment(record.accepted.order, order);
117
+ // Removing a criterion or gate lowers the bar in every reading, so an agent never certifies it. A rewrite may
118
+ // only clarify: where a model grades, the next grade judges it; under checks nothing could, so a person does.
119
+ // So does the amendment after a grade that found the agent's earlier ones lowered the bar.
120
+ const unjudgeable = policy.grading === 'checks' ? review.filter(row => checksBindsField(row.field)) : [];
121
+ const failedReview = record.amendmentsFailedReview;
122
+ const decider = policy.gates.amend === 'agent' && (weakened.length || unjudgeable.length || failedReview) ? 'person' : policy.gates.amend;
215
123
  const sources = await composition.sourceDigests(order);
216
124
  const digest = workOrderDigest(order, sources, record.accepted.base);
217
125
  return gated('amend', decider, [`Amend ${order.id} (digest ${record.accepted.digest} -> ${digest})`,
218
- ...weakened.length ? [`This weakens the accepted contract: ${weakened.join('; ')}`] : [], ...shownOrder], 'amend', async (by) => store.update(order.id, async (existing) => {
126
+ ...weakened.length ? [`This weakens the accepted contract: ${weakened.join('; ')}`] : [],
127
+ ...failedReview ? [`A grade found the agent's earlier amendments lower the bar: ${failedReview.note}`] : [],
128
+ ...review.map(row => `Changed ${row.field}: "${row.before}" -> "${row.after}"`), ...shownOrder], 'amend', async (by) => store.update(order.id, async (existing) => {
219
129
  if (!existing || existing.accepted.digest !== record.accepted.digest)
220
130
  throw new RefusalError(`${order.id} changed while this prompt was open; rerun work accept`);
221
131
  const next = { ...existing, state: 'accepted', attempts: 0, checks: undefined, grade: undefined,
132
+ ...by === 'agent' ? {} : { amendmentsFailedReview: undefined },
222
133
  accepted: { at: now(), by, digest, order, sources, base: existing.accepted.base },
223
- amendments: [...existing.amendments, { at: now(), by, from: existing.accepted.digest, to: digest, weakened: [...weakened] }] };
224
- return { record: next, value: complete(operation, { id: order.id, state: 'accepted', by, digest, amended: true, weakened }) };
134
+ amendments: [...existing.amendments, { at: now(), by, from: existing.accepted.digest, to: digest, weakened: [...weakened], review: [...review] }] };
135
+ return { record: next, value: complete(operation, { id: order.id, state: 'accepted', by, digest, amended: true, weakened,
136
+ ...by === 'agent' && review.length && policy.grading !== 'checks' ? { judgedAtGrade: review.map(row => row.field) } : {}, ...await relatedDecisions(context, order) }) };
225
137
  }));
226
138
  }
227
139
  if (request.operation === 'work-check') {
@@ -240,6 +152,8 @@ async function work(request, composition) {
240
152
  return passed ? complete(operation, value) : unsatisfied(operation, value, `${results.filter(row => !row.passed).map(row => `${row.criterion}: ${row.outcome}`).join('; ')}`);
241
153
  }
242
154
  if (request.operation === 'work-note') {
155
+ if (request.kind === 'decision' && !composition.docs?.decisions)
156
+ return refusal(operation, DECISIONS_UNDECLARED);
243
157
  await current(request.workOrderId).catch(async (error) => {
244
158
  // A note on a stale or blocked order is how its cause gets recorded, so only a closed or unaccepted one refuses.
245
159
  const record = await store.read(request.workOrderId);
@@ -249,10 +163,13 @@ async function work(request, composition) {
249
163
  const index = await store.update(request.workOrderId, async (existing) => {
250
164
  if (!existing)
251
165
  throw new RefusalError(`${request.workOrderId} is not accepted`);
252
- return { record: { ...existing, notes: [...existing.notes, { at: now(), by: 'agent', kind: request.question ? 'question' : 'note', text: request.text }] }, value: existing.notes.length + 1 };
166
+ return { record: { ...existing, notes: [...existing.notes, { at: now(), by: 'agent', kind: request.kind, text: request.text }] }, value: existing.notes.length + 1 };
253
167
  });
254
- return complete(operation, { id: request.workOrderId, note: index, ...request.question ? { question: index, state: 'blocked' } : {} });
168
+ return complete(operation, { id: request.workOrderId, note: index, ...request.kind === 'question' ? { question: index, state: 'blocked' } : {},
169
+ ...request.kind === 'decision' ? { decision: index, resolveWith: `npx --no cockpit work decide --config <your cockpit.toml> --work-order ${request.workOrderId} --decision ${index} --record` } : {} });
255
170
  }
171
+ if (request.operation === 'work-decide')
172
+ return decideOrder(request, context);
256
173
  if (request.operation === 'work-answer') {
257
174
  const record = await store.read(request.workOrderId);
258
175
  if (!record || TERMINAL.has(record.state))
@@ -267,117 +184,40 @@ async function work(request, composition) {
267
184
  value: complete(operation, { id: request.workOrderId, answered: request.question, by }) };
268
185
  }));
269
186
  }
270
- if (request.operation === 'work-grade') {
187
+ if (request.operation === 'work-grade')
188
+ return gradeOrder(request, context);
189
+ if (request.operation === 'work-acknowledge') {
271
190
  const { order, record } = await current(request.workOrderId);
272
- if (policy.grading === 'self' ? !request.verdict || !request.note : request.verdict !== undefined || request.note !== undefined) {
273
- return refusal(operation, policy.grading === 'self' ? '[work] grading = "self": pass --verdict met|unmet and --note <what you checked>'
274
- : `--verdict and --note apply only under [work] grading = "self"; this cockpit grades with ${policy.grading}`);
275
- }
276
- let verdicts;
277
- let evaluator;
278
- const unchecked = order.criteria.filter(criterion => !criterion.check).map(criterion => criterion.id);
279
- if (policy.grading === 'checks' && unchecked.length)
280
- return refusal(operation, `[work] grading = "checks" closes an order only when cockpit ran a check for every criterion; ${unchecked.join(', ')} declare none`);
281
- // Checks and scope bind in every mode, so an agent's own `met` covers neither a failed check nor a
282
- // change outside scope. Changes are read after the checks, so files a check writes count too.
283
- const checks = await runChecks(order);
284
- const changes = await composition.changes(record.accepted.base);
285
- const outOfScope = changes.map(change => change.path).filter(path => !scopeMatches(order.scope, path));
286
- if (policy.grading === 'self') {
287
- verdicts = order.criteria.map(criterion => ({ id: criterion.id, verdict: request.verdict, note: request.note }));
288
- }
289
- else if (policy.grading === 'checks') {
290
- verdicts = order.criteria.map(criterion => {
291
- const result = checks.find(row => row.criterion === criterion.id);
292
- return { id: criterion.id, verdict: result.passed ? 'met' : 'unmet', note: `cockpit ran the check: ${result.outcome}` };
293
- });
294
- }
295
- else {
296
- const graded = await composition.evaluate({ objective: order.objective, criteria: order.criteria, nonGoals: order.non_goals, scope: order.scope, checks, changes, outOfScope });
297
- if ('refusal' in graded)
298
- return refusal(operation, `${graded.refusal.code}: ${graded.refusal.message}; ${graded.refusal.fix}`);
299
- verdicts = graded.criteria;
300
- evaluator = { tool: graded.evaluator.tool, fallbacks: graded.evaluator.fallbacks.map(row => ({ ...row })) };
301
- }
302
- const passed = verdicts.every(row => row.verdict === 'met') && checks.every(row => row.passed) && outOfScope.length === 0;
303
- const grade = { at: now(), digest: record.accepted.digest, mode: policy.grading, passed,
304
- criteria: [...verdicts], outOfScope: [...outOfScope], ...evaluator ? { evaluator } : {}, ...request.note ? { note: request.note } : {} };
305
- await store.update(order.id, async (existing) => {
191
+ const weak = await composition.testWeakening(record.accepted.base);
192
+ if (!weak.findings.length)
193
+ return refusal(operation, `${order.id} weakens no tests since its base, so there is nothing to acknowledge`);
194
+ const digest = testWeakeningDigest(weak.findings);
195
+ if (record.acknowledgements.some(row => row.digest === digest))
196
+ return refusal(operation, `this weakening of ${order.id} is already acknowledged; run work grade`);
197
+ const rows = weak.findings.map(describeWeakening);
198
+ return gated('acknowledge', policy.gates.acknowledge, [`Acknowledge that ${order.id} weakens tests since its base (test files from ${weak.patterns.source}):`,
199
+ ...rows.map(row => ` ${row}`), 'Reason given:', ` ${request.note}`], 'acknowledge', async (by) => store.update(order.id, async (existing) => {
306
200
  if (!existing || existing.accepted.digest !== record.accepted.digest)
307
- throw new RefusalError(`${order.id} changed while it was graded; rerun work grade`);
308
- return { record: { ...existing, state: passed ? 'graded' : 'accepted', attempts: existing.attempts + (passed ? 0 : 1), grade,
309
- ...checks.length ? { checks: { at: now(), digest: record.accepted.digest, results: [...checks] } } : {} }, value: undefined };
310
- });
311
- const value = { id: order.id, state: passed ? 'graded' : 'accepted', mode: policy.grading, criteria: verdicts, checks, outOfScope, ...evaluator ? { evaluator } : {} };
312
- if (passed)
313
- return complete(operation, value);
314
- return unsatisfied(operation, value, [...verdicts.filter(row => row.verdict !== 'met').map(row => `${row.id} ${row.verdict}: ${row.note}`),
315
- ...checks.filter(row => !row.passed).map(row => `check ${row.criterion}: ${row.outcome}`),
316
- ...outOfScope.length ? [`changed outside scope: ${outOfScope.join(', ')}`] : []].join('; '));
317
- }
318
- if (request.operation === 'work-signoff') {
319
- const record = await store.read(request.workOrderId);
320
- if (!record)
321
- return refusal(operation, `${request.workOrderId} is not accepted`);
322
- if (TERMINAL.has(record.state))
323
- return refusal(operation, `${request.workOrderId} is already closed (${record.state})`);
324
- let how;
325
- if (request.close === 'done') {
326
- const { order } = await current(request.workOrderId);
327
- if (record.state !== 'graded' || !record.grade?.passed || record.grade.digest !== record.accepted.digest) {
328
- return refusal(operation, `${order.id} is ${record.state}, not graded; run npx --no cockpit work grade --config <your cockpit.toml> --work-order ${order.id}`);
201
+ throw new RefusalError(`${order.id} changed while this prompt was open; rerun work acknowledge`);
202
+ if (testWeakeningDigest((await composition.testWeakening(existing.accepted.base)).findings) !== digest) {
203
+ throw new RefusalError(`the test changes of ${order.id} changed while this prompt was open; rerun work acknowledge`);
329
204
  }
330
- const open = await Promise.all(order.depends_on.map(async (dependency) => (await store.read(dependency))?.state === 'done' ? undefined : dependency));
331
- if (open.some(Boolean))
332
- return refusal(operation, `${order.id} depends on ${open.filter(Boolean).join(', ')}, which ${open.filter(Boolean).length === 1 ? 'is' : 'are'} not done`);
333
- how = CLOSURE[record.grade.mode];
334
- }
335
- else {
336
- if (!request.note)
337
- return refusal(operation, `closing ${request.workOrderId} as ${request.close} needs --note saying what was and was not done`);
338
- how = request.close;
339
- }
340
- return gated('signoff', policy.gates.signoff, [`Close ${request.workOrderId} as ${request.close} (${how})`, ...request.note ? [`Note: ${request.note}`] : [],
341
- ...record.grade ? record.grade.criteria.map(row => `${row.id}: ${row.verdict} — ${row.note}`) : []], 'signoff', async (by) => store.update(request.workOrderId, async (existing) => {
342
- if (!existing || existing.state !== record.state || existing.accepted.digest !== record.accepted.digest)
343
- throw new RefusalError(`${request.workOrderId} changed while this prompt was open`);
344
- const state = request.close;
345
- return { record: { ...existing, state, closure: { at: now(), by, how, ...request.note ? { note: request.note } : {} } },
346
- value: complete(operation, { id: request.workOrderId, state, closedAs: how, by }) };
205
+ return { record: { ...existing, acknowledgements: [...existing.acknowledgements, { at: now(), by, digest, findings: rows, note: request.note }] },
206
+ value: complete(operation, { id: order.id, acknowledged: rows, by, digest }) };
347
207
  }));
348
208
  }
349
- if (request.operation === 'work-recover')
350
- return complete(operation, await store.recover(request.workOrderId, request.apply));
351
- if (request.operation === 'work-verify') {
352
- const problems = [];
353
- const awaiting = [];
354
- for (const record of await store.list()) {
355
- const row = await observe(record.id);
356
- if (!TERMINAL.has(record.state) && row.digest !== record.accepted.digest)
357
- problems.push(`${record.id} changed since it was accepted; accept the change or revert it`);
358
- if (record.state === 'done' && (!record.grade?.passed || record.grade.digest !== record.accepted.digest || !record.closure))
359
- problems.push(`${record.id} is done without a passing grade of its accepted contract`);
360
- awaiting.push(...pendingCi(record).map(step => `${record.id} ${step}`));
361
- }
362
- const ci = runningInCi(composition.env);
363
- const value = { ci, problems, ...ci ? { confirmed: awaiting } : { awaitingCi: awaiting } };
364
- return problems.length ? unsatisfied(operation, value, problems.join('; ')) : complete(operation, value);
209
+ if (request.operation === 'work-signoff')
210
+ return signoff(request, context);
211
+ if (request.operation === 'work-recover') {
212
+ // A pid probe sees only this machine's processes, so the command looks where it cannot first: the
213
+ // lock's host and the containers cockpit labels. Applied, it removes nothing it has not shown gone.
214
+ const recovered = await store.recover(request.workOrderId, request.apply, composition.containers);
215
+ if (!request.apply || recovered.lock === 'none' || recovered.lock === 'removed')
216
+ return complete(operation, recovered);
217
+ return refusal(operation, `the lock on ${request.workOrderId} was left in place: its holder ${recovered.lock === 'live' ? 'is still running' : 'cannot be shown gone'} `
218
+ + `(${recovered.checked?.join('; ')}); wait for it to finish, or stop it on the host that holds it, then rerun work recover --apply`);
365
219
  }
366
- // grade-criteria: the quick path, an independent grade of uncommitted work with no order and no record.
367
- const parsed = await composition.readCriteriaFile(request.criteriaFile);
368
- if (!parsed.ok)
369
- return refusal(operation, `${request.criteriaFile} is not a criteria file: ${parsed.problems.join('; ')}`);
370
- const file = parsed.value;
371
- const checks = await runChecks(file);
372
- // The criteria file is the request, not part of the change it grades.
373
- const changes = (await composition.changes(await composition.heads())).filter(change => change.path !== posix.normalize(request.criteriaFile));
374
- const outOfScope = file.scope.length ? changes.map(change => change.path).filter(path => !scopeMatches(file.scope, path)) : [];
375
- const subject = { ...file.objective ? { objective: file.objective } : {}, criteria: file.criteria, nonGoals: [], scope: file.scope, checks, changes, outOfScope };
376
- const graded = await composition.evaluate(subject);
377
- if ('refusal' in graded)
378
- return refusal(operation, `${graded.refusal.code}: ${graded.refusal.message}; ${graded.refusal.fix}`);
379
- const passed = graded.criteria.every(row => row.verdict === 'met') && checks.every(row => row.passed) && outOfScope.length === 0;
380
- const value = { criteria: graded.criteria, checks, outOfScope, evaluator: graded.evaluator };
381
- return passed ? complete(operation, value) : unsatisfied(operation, value, graded.criteria.filter(row => row.verdict !== 'met').map(row => `${row.id} ${row.verdict}: ${row.note}`)
382
- .concat(checks.filter(row => !row.passed).map(row => `check ${row.criterion}: ${row.outcome}`), outOfScope.length ? [`changed outside scope: ${outOfScope.join(', ')}`] : []).join('; '));
220
+ if (request.operation === 'work-verify')
221
+ return verify(context);
222
+ return gradeCriteriaFile(request, context);
383
223
  }
@@ -1,52 +1,12 @@
1
1
  import { type EvaluatorTool } from '../schema.js';
2
- import type { ConsumedWorkEnvironmentConfig } from '../config/load.js';
3
2
  import { type EffectivePolicy } from '../autonomy.js';
4
- import { type EvaluatorChain } from '../grading/evaluate.js';
5
3
  import { type ChangedFile, type GradingSubject, type Verdict } from '../grading/grade.js';
6
- import { type BrowserCommandInput, type BrowserCommandKind, type BrowserRoutePolicy } from '../browser/commands.js';
7
- import { type BrowserRuntimeProjection } from '../browser/runtime.js';
8
- import { type BrowserProjectClient } from '../browser/surfaces.js';
9
- import type { EnvironmentSnapshot, RuntimeEvidence } from '../environment/evidence.js';
4
+ import { type BrowserCommandInput, type BrowserCommandKind } from '../browser/commands.js';
5
+ import { type RepositoryWeakening, type TestFilePatterns } from '../testing/weakening.js';
10
6
  import { type GateDependencies, type GateRegistry, type GateRequest } from '../testing/gates.js';
11
7
  import { parseCriteriaFile, type ParsedFile, type WorkOrder, type WorkOrderCheck } from './contract.js';
12
- import { type CheckResult, type WorkStore } from './store.js';
13
- export type WorkEnvironmentApplicationConfig = ConsumedWorkEnvironmentConfig;
14
- type BrowserAuthority = Readonly<{
15
- runtime: BrowserRuntimeProjection;
16
- client: BrowserProjectClient;
17
- policy: BrowserRoutePolicy;
18
- state?: BrowserCommandInput['state'];
19
- }>;
20
- type EvaluatorAuthority = Readonly<{
21
- path?: string;
22
- bytes: string;
23
- sha256: string;
24
- restriction: 'instructed';
25
- executableDirectories?: readonly string[];
26
- chain: EvaluatorChain;
27
- environment?: Readonly<Record<string, string | undefined>>;
28
- }>;
29
- export interface WorkApplicationAuthorities {
30
- readonly browser?: BrowserAuthority;
31
- readonly resolveBrowser?: () => Promise<BrowserAuthority>;
32
- readonly testing?: Readonly<{
33
- registry: GateRegistry;
34
- dependencies: GateDependencies;
35
- runtime?: () => Promise<Readonly<{
36
- stack: NonNullable<GateRequest['stack']>;
37
- evidence?: Readonly<{
38
- snapshot: EnvironmentSnapshot;
39
- capture: () => Promise<RuntimeEvidence>;
40
- }>;
41
- }>>;
42
- }>;
43
- readonly evaluator?: EvaluatorAuthority;
44
- readonly resolveEvaluator?: () => Promise<EvaluatorAuthority>;
45
- readonly executableDirectories?: readonly string[];
46
- readonly declaredEnvironment?: Readonly<Record<string, string>>;
47
- readonly env: Readonly<Record<string, string | undefined>>;
48
- }
49
- export type GradingResult = Readonly<{
8
+ import { type CheckResult, type ContainerEvidence, type WorkStore } from './store.js';
9
+ type GradingResult = Readonly<{
50
10
  criteria: readonly Verdict[];
51
11
  evaluator: Readonly<{
52
12
  tool: string;
@@ -78,16 +38,43 @@ export interface WorkApplicationComposition {
78
38
  readonly sourceDigests: (order: Pick<WorkOrder, 'criteria'>) => Promise<Readonly<Record<string, string>>>;
79
39
  readonly heads: () => Promise<Readonly<Record<string, string>>>;
80
40
  readonly runCheck: (criterion: string, check: WorkOrderCheck) => Promise<CheckResult>;
81
- readonly changes: (base: Readonly<Record<string, string>>) => Promise<readonly ChangedFile[]>;
82
41
  readonly evaluate: (subject: GradingSubject) => Promise<GradingResult>;
83
- readonly browser: (kind: BrowserCommandKind, role?: string) => Promise<BrowserCommandInput>;
42
+ /** Test weakening in each checkout since `base`, over the files this cockpit treats as tests. */
43
+ readonly testWeakening: (base: Readonly<Record<string, string>>) => Promise<TestWeakeningObservation>;
44
+ /** What changed in each checkout since `base` (committed, staged, unstaged and untracked, each with its diff), and its test weakening, from one listing of untracked files. */
45
+ readonly review: (base: Readonly<Record<string, string>>) => Promise<Readonly<{
46
+ changes: readonly ChangedFile[];
47
+ weakening: TestWeakeningObservation;
48
+ }>>;
49
+ readonly browser: (kind: BrowserCommandKind, role?: string, route?: string) => Promise<BrowserCommandInput>;
50
+ /** The [docs] table as declared; absent when the cockpit declares none. */
51
+ readonly docs?: Readonly<{
52
+ decisions?: string;
53
+ decisionsIndex?: string;
54
+ }>;
55
+ /** Each Markdown file in [docs] decisions, by reference, with its text; empty when the directory does not exist yet. */
56
+ readonly decisionRecords: () => Promise<ReadonlyMap<string, string>>;
57
+ /** A decision record's or the index's text, or undefined when the file does not exist. */
58
+ readonly readDocsFile: (reference: string) => Promise<string | undefined>;
59
+ /** Writes each record, a new one exclusively and an existing one replaced, then appends `indexLine` to [docs] decisions_index. */
60
+ readonly writeDecisionRecords: (writes: readonly Readonly<{
61
+ reference: string;
62
+ text: string;
63
+ create: boolean;
64
+ }>[], indexLine?: string) => Promise<void>;
84
65
  readonly gate: (request: WorkApplicationGateRequest) => Promise<Readonly<{
85
66
  registry: GateRegistry;
86
67
  request: GateRequest;
87
68
  dependencies: GateDependencies;
88
69
  }>>;
70
+ /** The containers cockpit labels on this machine's local Docker daemon, where a lock's holder may run unseen by a pid probe. */
71
+ readonly containers: () => Promise<ContainerEvidence>;
89
72
  }
90
- export interface WorkApplicationGateRequest {
73
+ type TestWeakeningObservation = Readonly<{
74
+ patterns: TestFilePatterns;
75
+ findings: readonly RepositoryWeakening[];
76
+ }>;
77
+ interface WorkApplicationGateRequest {
91
78
  readonly operation: 'test-run';
92
79
  readonly gate: string;
93
80
  readonly mode: 'canonical' | 'full' | 'staged';
@@ -96,18 +83,8 @@ export interface WorkApplicationGateRequest {
96
83
  value: string;
97
84
  }>;
98
85
  }
99
- export type VerificationObservation = Readonly<{
100
- kind: 'executed';
101
- exitCode: number;
102
- stdout: string;
103
- stderr: string;
104
- truncated: boolean;
105
- }> | Readonly<{
106
- kind: 'error' | 'unavailable';
107
- reason: string;
108
- }>;
109
86
  export declare function isWorkApplicationComposition(value: unknown): value is WorkApplicationComposition;
110
- export type ConfiguredWorkApplicationCompositionResult = Readonly<{
87
+ type ConfiguredWorkApplicationCompositionResult = Readonly<{
111
88
  kind: 'complete';
112
89
  composition: WorkApplicationComposition;
113
90
  }> | Readonly<{