@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
@@ -0,0 +1,212 @@
1
+ // Grading: the standing criteria cockpit adds to every grade, the test protection a grade and a test
2
+ // run apply, `work grade` against an order's accepted contract and `grade --criteria` with no order.
3
+ import { posix } from 'node:path';
4
+ import { RefusalError } from '../environment/contract.js';
5
+ import { runGate } from '../testing/gates.js';
6
+ import { describeWeakening, testWeakeningDigest } from '../testing/weakening.js';
7
+ import { scopeMatcher } from './contract.js';
8
+ import { complete, now, refusal, TERMINAL, unsatisfied } from './application-context.js';
9
+ import { decisions } from './application-decisions.js';
10
+ // A list in a criterion's text stays readable; the full list is in the grade's result beside it.
11
+ const listed = (rows, separator, limit = 3_000) => {
12
+ const text = rows.join(separator);
13
+ return text.length <= limit ? text : `${text.slice(0, limit)} (cut; ${rows.length} in all)`;
14
+ };
15
+ const weakeningPath = (row) => row.repository === '.' ? row.path : `${row.repository}:${row.path}`;
16
+ // The agent's contract amendments since a person or CI last accepted it, each still to be judged by the next grade.
17
+ function amendmentsToReview(record) {
18
+ const reviewed = record.amendments.findLastIndex(row => row.by !== 'agent');
19
+ return record.amendments.slice(reviewed + 1).flatMap(row => row.review);
20
+ }
21
+ // The paths a self-reported note does not name, alone or by a directory above them.
22
+ const unaddressed = (note, paths) => paths.filter(path => {
23
+ const parts = path.split('/');
24
+ return !parts.some((_, index) => note.includes(index === parts.length - 1 ? path : `${parts.slice(0, index + 1).join('/')}/`));
25
+ });
26
+ const ACKNOWLEDGE = 'npx --no cockpit work acknowledge --config <your cockpit.toml> --work-order <id> --note <why this is not a weakening>';
27
+ // Tests weakened since the base, judged by rule where no model reads the change. An acknowledgement
28
+ // covers exactly the findings it was recorded for, so any further weakening fails again.
29
+ function protection(weak, record, gate) {
30
+ const rows = weak.findings.map(describeWeakening);
31
+ const digest = testWeakeningDigest(weak.findings);
32
+ const acknowledged = weak.findings.length ? record?.acknowledgements.find(row => row.digest === digest) : undefined;
33
+ const how = !record ? 'a work order lets it be acknowledged; there is none for this grade'
34
+ : gate === 'person' ? `a person records it with ${ACKNOWLEDGE} --interactive` : gate === 'ci' ? `acknowledge it with ${ACKNOWLEDGE}, which the work verify job confirms in CI`
35
+ : `acknowledge it with ${ACKNOWLEDGE}`;
36
+ const verdict = !weak.findings.length ? undefined
37
+ : acknowledged ? { verdict: 'met', note: `weakening acknowledged by ${acknowledged.by}: ${acknowledged.note}`.slice(0, 4_000) }
38
+ : { verdict: 'unmet', note: `tests were weakened since the base (${rows.join('; ')}; test files from ${weak.patterns.source}); revert it, or ${how}`.slice(0, 4_000) };
39
+ return { verdict, value: { testFiles: weak.patterns.source, findings: rows, ...acknowledged ? { acknowledgedBy: acknowledged.by } : {} } };
40
+ }
41
+ const TESTS_JUSTIFIED = 'test-changes-justified';
42
+ const AMENDMENTS_HOLD = 'amendments-hold-bar';
43
+ const STANDING = [
44
+ { id: 'decisions-recorded', text: ({ docs, record }) => {
45
+ if (!docs?.decisions || !record)
46
+ return undefined;
47
+ const recorded = decisions(record).map(row => `${row.decision} (${row.title})${row.resolution?.record ? ` in ${row.resolution.record}` : ''}`).join('; ');
48
+ return ('Every lasting decision this change makes (a choice a later change must know about, such as a format, a boundary, '
49
+ + `a dependency or a rejected option) is recorded on the work order or in a decision record under ${docs.decisions}. Recorded on this order: ${recorded || 'none'}.`).slice(0, 2_000);
50
+ } },
51
+ { id: 'docs-updated', text: ({ docs }) => docs ? 'Documentation that describes behaviour this change alters is updated in the change, or no documentation describes it.' : undefined },
52
+ // Which paths changed outside scope is a git fact; whether each change was needed is a judgement.
53
+ { id: 'scope-changes-necessary', text: ({ outOfScope }) => outOfScope.length ? 'Each change outside the scope is necessary to the objective and harmless, such as a lockfile, a regenerated '
54
+ + 'reference or a changeset the in-scope change requires; any other is scope creep and fails this. The note names each path and says why. '
55
+ + `Changed outside scope: ${listed(outOfScope, ', ')}.` : undefined },
56
+ // The detector names candidates; whether each change keeps the tests' force is a judgement.
57
+ { id: TESTS_JUSTIFIED, weakening: true, text: ({ weak, record }) => {
58
+ if (!weak.findings.length)
59
+ return undefined;
60
+ const acknowledged = record?.acknowledgements.map(row => `${row.by}: ${row.note}`) ?? [];
61
+ return 'Each test change flagged below keeps the tests\' force: a removed or loosened test is replaced by one at least '
62
+ + 'as strict, or tested behaviour the change itself removes. A test weakened to make the change pass fails this. The note names each file and says why. '
63
+ + `Flagged (test files from ${weak.patterns.source}): ${listed(weak.findings.map(describeWeakening), '; ')}. `
64
+ + `Acknowledgements recorded on the order: ${acknowledged.length ? listed(acknowledged, '; ', 1_000) : 'none'}.`;
65
+ }, underChecks: { id: 'test-protection', verdict: ({ weak, record, acknowledge }) => protection(weak, record, acknowledge).verdict } },
66
+ // An agent may reword its own contract; whether the rewording lowers the bar is a judgement. Under
67
+ // checks nothing could judge it, so the amend gate passes such an amendment to a person instead.
68
+ { id: AMENDMENTS_HOLD, text: ({ amendments }) => amendments.length ? 'The amendments an agent made to the accepted contract do not lower the bar the work is held to: each clarifies or '
69
+ + 'tightens what it replaces, and none excuses work the original asked for. Before and after: '
70
+ + `${listed(amendments.map(row => `${row.field}: "${row.before}" -> "${row.after}"`), '; ')}.` : undefined,
71
+ remedy: id => `; the next amendment of ${id} is a person's: revert or rewrite workorders/${id}.md, then a person runs npx --no cockpit work accept --config <your cockpit.toml> --work-order ${id} --interactive` },
72
+ ];
73
+ const RESERVED = new Set(STANDING.map(row => row.id));
74
+ const standingCriteria = (input) => STANDING.flatMap(row => {
75
+ const text = row.text(input);
76
+ return text === undefined ? [] : [{ id: row.id, text }];
77
+ });
78
+ const standingVerdictsUnderChecks = (input) => STANDING.flatMap(row => {
79
+ const reached = row.underChecks?.verdict(input);
80
+ return reached ? [{ id: row.underChecks.id, ...reached }] : [];
81
+ });
82
+ const WEAKENING_IDS = new Set(STANDING.flatMap(row => row.weakening ? [row.id, ...row.underChecks ? [row.underChecks.id] : []] : []));
83
+ const remedyOf = (id, orderId) => STANDING.find(row => row.id === id)?.remedy?.(orderId) ?? '';
84
+ const split = (verdicts) => ({ others: verdicts.filter(row => row.verdict !== 'met' && !WEAKENING_IDS.has(row.id)),
85
+ weakening: verdicts.filter(row => WEAKENING_IDS.has(row.id) && row.verdict !== 'met').map(row => `${row.id} ${row.verdict}: ${row.note}`).join('; ') || undefined });
86
+ // A test run's result, held to the protection of every open order.
87
+ export async function testRun(captured, composition) {
88
+ const operation = captured.operation;
89
+ const prepared = await composition.gate(captured);
90
+ const run = await runGate(prepared.registry, prepared.request, prepared.dependencies);
91
+ // A passing run proves nothing if the tests it ran were weakened, so it is checked against the base of each open order.
92
+ // Under checks grading an unacknowledged finding fails the run; where a model grades, the grade judges it.
93
+ const open = (await composition.store.list()).filter(row => !TERMINAL.has(row.state));
94
+ // Orders accepted at the same commits share one observation of what changed since.
95
+ const observed = new Map();
96
+ const weakeningSince = (base) => {
97
+ const key = JSON.stringify(Object.entries(base).sort(([left], [right]) => left < right ? -1 : 1));
98
+ if (!observed.has(key))
99
+ observed.set(key, composition.testWeakening(base));
100
+ return observed.get(key);
101
+ };
102
+ const guards = await Promise.all(open.map(async (row) => ({ id: row.id, ...protection(await weakeningSince(row.accepted.base), row, composition.policy.gates.acknowledge) })));
103
+ const binding = composition.policy.grading === 'checks';
104
+ const testProtection = guards.length ? { comparedWith: guards.map(row => `${row.id}'s base`), orders: guards.map(row => ({ id: row.id, ...row.value })),
105
+ ...!binding && guards.some(row => row.value.findings.length) ? { note: `under [work] grading = "${composition.policy.grading}" each order's grade judges these findings as ${TESTS_JUSTIFIED}` } : {} }
106
+ : { comparedWith: [], note: 'no open work order records a base commit to compare tests with; accept an order before changing tests' };
107
+ const weakening = binding ? guards.flatMap(row => row.verdict?.verdict === 'unmet' ? [`${row.id}: ${row.verdict.note}`] : []).join(' | ') : '';
108
+ const failure = run.satisfiesGate ? '' : `${run.verdict}: ${run.reasons.join('; ') || 'declared gate was not satisfied'}`;
109
+ return !failure && !weakening ? complete(operation, { ...run, testProtection }) : unsatisfied(operation, { ...run, testProtection }, failure, weakening || undefined);
110
+ }
111
+ export async function gradeOrder(request, context) {
112
+ const { composition, operation, policy, store, current, runChecks } = context;
113
+ const { order, record } = await current(request.workOrderId);
114
+ if (policy.grading === 'self' ? !request.verdict || !request.note : request.verdict !== undefined || request.note !== undefined) {
115
+ return refusal(operation, policy.grading === 'self' ? '[work] grading = "self": pass --verdict met|unmet and --note <what you checked>'
116
+ : `--verdict and --note apply only under [work] grading = "self"; this cockpit grades with ${policy.grading}`);
117
+ }
118
+ let verdicts;
119
+ let evaluator;
120
+ const unchecked = order.criteria.filter(criterion => !criterion.check).map(criterion => criterion.id);
121
+ if (policy.grading === 'checks' && unchecked.length)
122
+ return refusal(operation, `[work] grading = "checks" closes an order only when cockpit ran a check for every criterion; ${unchecked.join(', ')} declare none`);
123
+ const judged = policy.grading !== 'checks';
124
+ const taken = judged ? order.criteria.filter(criterion => RESERVED.has(criterion.id)).map(criterion => criterion.id) : [];
125
+ if (taken.length)
126
+ return refusal(operation, `${order.id} names a criterion ${taken.join(', ')}, which cockpit adds to grades itself; rename it in workorders/${order.id}.md`);
127
+ // Checks bind in every mode, so an agent's own `met` never covers a failed check. Changes are read
128
+ // after the checks, so files a check writes count too.
129
+ const checks = await runChecks(order);
130
+ const { changes, weakening: weak } = await composition.review(record.accepted.base);
131
+ // The decision records an order is required to write are part of its work wherever scope points.
132
+ const decisionFiles = [composition.docs?.decisions, composition.docs?.decisionsIndex].filter((path) => path !== undefined);
133
+ const inScope = scopeMatcher([...order.scope, ...decisionFiles]);
134
+ const outOfScope = changes.map(change => change.path).filter(path => !inScope(path));
135
+ const guarded = protection(weak, record, policy.gates.acknowledge);
136
+ const amendments = judged ? amendmentsToReview(record) : [];
137
+ const standingInput = { docs: composition.docs, record, outOfScope, weak, amendments, acknowledge: policy.gates.acknowledge };
138
+ const standing = judged ? standingCriteria(standingInput) : [];
139
+ if (policy.grading === 'self') {
140
+ // The agent's verdict covers the standing criteria too, so its note must at least name what they list.
141
+ const missing = unaddressed(request.note, [...outOfScope, ...weak.findings.map(weakeningPath)]);
142
+ if (missing.length)
143
+ return refusal(operation, `[work] grading = "self": --note must name each path changed outside scope and each flagged test file, or a directory above it, `
144
+ + `and say why its change is needed; it does not name ${listed(missing, ', ', 1_000)}`);
145
+ verdicts = [...order.criteria, ...standing].map(criterion => ({ id: criterion.id, verdict: request.verdict, note: request.note }));
146
+ }
147
+ else if (policy.grading === 'checks') {
148
+ verdicts = order.criteria.map(criterion => {
149
+ const result = checks.find(row => row.criterion === criterion.id);
150
+ return { id: criterion.id, verdict: result.passed ? 'met' : 'unmet', note: `cockpit ran the check: ${result.outcome}` };
151
+ });
152
+ // No model reads the change under checks, so weakened tests fail the grade until acknowledged.
153
+ verdicts = [...verdicts, ...standingVerdictsUnderChecks(standingInput)];
154
+ }
155
+ else {
156
+ // The contract's own change is shown to the evaluator as a changed file, so a verdict on the amendments can quote it.
157
+ const contract = amendments.length ? [{ path: `workorders/${order.id}.md`, status: 'amended',
158
+ diff: amendments.map(row => `${row.field}\n- ${row.before}\n+ ${row.after}`).join('\n') }] : [];
159
+ const graded = await composition.evaluate({ objective: order.objective, criteria: [...order.criteria, ...standing], nonGoals: order.non_goals, scope: order.scope,
160
+ checks, changes: [...changes, ...contract], outOfScope });
161
+ if ('refusal' in graded)
162
+ return refusal(operation, `${graded.refusal.code}: ${graded.refusal.message}; ${graded.refusal.fix}`);
163
+ verdicts = graded.criteria;
164
+ evaluator = { tool: graded.evaluator.tool, fallbacks: graded.evaluator.fallbacks.map(row => ({ ...row })) };
165
+ }
166
+ // Where a model grades, whether a change outside scope was needed is its standing criterion; under checks nothing can judge it, so any fails.
167
+ const passed = verdicts.every(row => row.verdict === 'met') && checks.every(row => row.passed) && (judged || outOfScope.length === 0);
168
+ const grade = { at: now(), digest: record.accepted.digest, mode: policy.grading, passed,
169
+ criteria: [...verdicts], outOfScope: [...outOfScope], ...evaluator ? { evaluator } : {}, ...request.note ? { note: request.note } : {} };
170
+ const loweredBar = verdicts.find(row => row.id === AMENDMENTS_HOLD && row.verdict !== 'met');
171
+ await store.update(order.id, async (existing) => {
172
+ if (!existing || existing.accepted.digest !== record.accepted.digest)
173
+ throw new RefusalError(`${order.id} changed while it was graded; rerun work grade`);
174
+ return { record: { ...existing, state: passed ? 'graded' : 'accepted', attempts: existing.attempts + (passed ? 0 : 1), grade,
175
+ ...loweredBar ? { amendmentsFailedReview: { at: grade.at, note: loweredBar.note } } : {},
176
+ ...checks.length ? { checks: { at: now(), digest: record.accepted.digest, results: [...checks] } } : {} }, value: undefined };
177
+ });
178
+ const value = { id: order.id, state: passed ? 'graded' : 'accepted', mode: policy.grading, criteria: verdicts, checks, outOfScope, testProtection: guarded.value, ...evaluator ? { evaluator } : {} };
179
+ if (passed)
180
+ return complete(operation, value);
181
+ return unsatisfied(operation, value, [...split(verdicts).others.map(row => `${row.id} ${row.verdict}: ${row.note}${remedyOf(row.id, order.id)}`),
182
+ ...checks.filter(row => !row.passed).map(row => `check ${row.criterion}: ${row.outcome}`),
183
+ ...!judged && outOfScope.length ? [`changed outside scope: ${outOfScope.join(', ')}`] : []].join('; '), split(verdicts).weakening);
184
+ }
185
+ // The quick path: an independent grade of uncommitted work with no order and no record.
186
+ export async function gradeCriteriaFile(request, context) {
187
+ const { composition, operation, policy, runChecks } = context;
188
+ const parsed = await composition.readCriteriaFile(request.criteriaFile);
189
+ if (!parsed.ok)
190
+ return refusal(operation, `${request.criteriaFile} is not a criteria file: ${parsed.problems.join('; ')}`);
191
+ const file = parsed.value;
192
+ const taken = file.criteria.filter(criterion => RESERVED.has(criterion.id)).map(criterion => criterion.id);
193
+ if (taken.length)
194
+ return refusal(operation, `${request.criteriaFile} names a criterion ${taken.join(', ')}, which cockpit adds to grades itself; rename it`);
195
+ const checks = await runChecks(file);
196
+ // The criteria file is the request, not part of the change it grades.
197
+ // With no order there is no recorded base, so the change since HEAD is compared, and the evaluator judges any test weakening.
198
+ const review = await composition.review(await composition.heads());
199
+ const changes = review.changes.filter(change => change.path !== posix.normalize(request.criteriaFile));
200
+ const inScope = scopeMatcher(file.scope);
201
+ const outOfScope = file.scope.length ? changes.map(change => change.path).filter(path => !inScope(path)) : [];
202
+ const weak = review.weakening;
203
+ const criteria = [...file.criteria, ...standingCriteria({ outOfScope, weak, amendments: [], acknowledge: policy.gates.acknowledge })];
204
+ const subject = { ...file.objective ? { objective: file.objective } : {}, criteria, nonGoals: [], scope: file.scope, checks, changes, outOfScope };
205
+ const graded = await composition.evaluate(subject);
206
+ if ('refusal' in graded)
207
+ return refusal(operation, `${graded.refusal.code}: ${graded.refusal.message}; ${graded.refusal.fix}`);
208
+ const passed = graded.criteria.every(row => row.verdict === 'met') && checks.every(row => row.passed);
209
+ const value = { criteria: graded.criteria, checks, outOfScope, testProtection: { testFiles: weak.patterns.source, findings: weak.findings.map(describeWeakening) }, evaluator: graded.evaluator };
210
+ return passed ? complete(operation, value) : unsatisfied(operation, value, split(graded.criteria).others.map(row => `${row.id} ${row.verdict}: ${row.note}`)
211
+ .concat(checks.filter(row => !row.passed).map(row => `check ${row.criterion}: ${row.outcome}`)).join('; '), split(graded.criteria).weakening);
212
+ }
@@ -1,99 +1,6 @@
1
- import { z } from 'zod';
1
+ import { type BrowserCommandResult } from '../browser/commands.js';
2
2
  import { type WorkApplicationComposition } from './composition.js';
3
- declare const Request: z.ZodDiscriminatedUnion<[z.ZodObject<{
4
- operation: z.ZodLiteral<"work-status">;
5
- workOrderId: z.ZodOptional<z.ZodString>;
6
- }, z.core.$strict>, z.ZodObject<{
7
- operation: z.ZodLiteral<"work-accept">;
8
- workOrderId: z.ZodString;
9
- }, z.core.$strict>, z.ZodObject<{
10
- operation: z.ZodLiteral<"work-check">;
11
- workOrderId: z.ZodString;
12
- }, z.core.$strict>, z.ZodObject<{
13
- operation: z.ZodLiteral<"work-note">;
14
- workOrderId: z.ZodString;
15
- text: z.ZodString;
16
- question: z.ZodBoolean;
17
- }, z.core.$strict>, z.ZodObject<{
18
- operation: z.ZodLiteral<"work-answer">;
19
- workOrderId: z.ZodString;
20
- question: z.ZodNumber;
21
- text: z.ZodString;
22
- }, z.core.$strict>, z.ZodObject<{
23
- operation: z.ZodLiteral<"work-grade">;
24
- workOrderId: z.ZodString;
25
- verdict: z.ZodOptional<z.ZodEnum<{
26
- met: "met";
27
- unmet: "unmet";
28
- }>>;
29
- note: z.ZodOptional<z.ZodString>;
30
- }, z.core.$strict>, z.ZodObject<{
31
- operation: z.ZodLiteral<"work-signoff">;
32
- workOrderId: z.ZodString;
33
- close: z.ZodEnum<{
34
- abandoned: "abandoned";
35
- done: "done";
36
- partial: "partial";
37
- }>;
38
- note: z.ZodOptional<z.ZodString>;
39
- }, z.core.$strict>, z.ZodObject<{
40
- operation: z.ZodLiteral<"work-recover">;
41
- workOrderId: z.ZodString;
42
- apply: z.ZodBoolean;
43
- }, z.core.$strict>, z.ZodObject<{
44
- operation: z.ZodLiteral<"work-verify">;
45
- }, z.core.$strict>, z.ZodObject<{
46
- operation: z.ZodLiteral<"grade-criteria">;
47
- criteriaFile: z.ZodString;
48
- }, z.core.$strict>, z.ZodObject<{
49
- operation: z.ZodEnum<{
50
- "browser-drive": "browser-drive";
51
- "browser-screenshot": "browser-screenshot";
52
- }>;
53
- role: z.ZodOptional<z.ZodString>;
54
- }, z.core.$strict>, z.ZodObject<{
55
- operation: z.ZodLiteral<"test-run">;
56
- gate: z.ZodString;
57
- mode: z.ZodEnum<{
58
- canonical: "canonical";
59
- full: "full";
60
- staged: "staged";
61
- }>;
62
- select: z.ZodOptional<z.ZodObject<{
63
- kind: z.ZodEnum<{
64
- file: "file";
65
- route: "route";
66
- spec: "spec";
67
- test: "test";
68
- }>;
69
- value: z.ZodString;
70
- }, z.core.$strict>>;
71
- }, z.core.$strict>], "operation">;
72
- export type WorkApplicationRequest = z.infer<typeof Request>;
73
- type Operation = WorkApplicationRequest['operation'];
74
- export declare function captureWorkApplicationRequest(value: unknown): WorkApplicationRequest | undefined;
75
- export interface WorkApplicationContinuation {
76
- readonly digest: string;
77
- readonly prompt: string;
78
- readonly decide: (answer: string) => Promise<WorkApplicationOutcome>;
79
- }
80
- export type WorkApplicationOutcome = Readonly<{
81
- kind: 'complete';
82
- operation: Operation;
83
- value: unknown;
84
- }> | Readonly<{
85
- kind: 'human-needed';
86
- operation: Operation;
87
- continuation: WorkApplicationContinuation;
88
- }> | Readonly<{
89
- kind: 'refusal';
90
- operation: Operation | 'invalid';
91
- reason: string;
92
- }> | Readonly<{
93
- kind: 'unsatisfied';
94
- operation: Operation;
95
- value: unknown;
96
- reason: string;
97
- }>;
3
+ import { type WorkApplicationOutcome, type WorkApplicationRequest } from './application-context.js';
4
+ export { captureWorkApplicationRequest, type WorkApplicationOutcome, type WorkApplicationRequest } from './application-context.js';
98
5
  export declare function runWorkApplication(request: WorkApplicationRequest, composition: WorkApplicationComposition): Promise<WorkApplicationOutcome>;
99
- export {};
6
+ export declare function browserOutcome(operation: 'browser-drive' | 'browser-screenshot', result: BrowserCommandResult, failOnFindings: boolean): WorkApplicationOutcome;