@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
@@ -16,7 +16,7 @@ export interface GuideObservation {
16
16
  readonly contentSha256: string;
17
17
  readonly contentIdentity: string;
18
18
  }
19
- export type GuideObservationResult = {
19
+ type GuideObservationResult = {
20
20
  readonly kind: 'complete';
21
21
  readonly observations: readonly GuideObservation[];
22
22
  readonly missing: readonly string[];
@@ -24,7 +24,7 @@ export type GuideObservationResult = {
24
24
  readonly kind: 'refusal' | 'unavailable';
25
25
  readonly diagnostic: ProjectionDiagnostic;
26
26
  };
27
- export interface ProjectionSiblingInput {
27
+ interface ProjectionSiblingInput {
28
28
  slug: string;
29
29
  mount: Mount;
30
30
  directoryAuthority: string;
@@ -58,21 +58,21 @@ export interface ProjectionPlannerInput {
58
58
  /** The tools cockpit.toml lists; grants and start-up costs cover exactly these. Every shipped tool when absent. */
59
59
  targets?: readonly SurfaceTarget[];
60
60
  }
61
- export interface LoadCostCharacterization {
61
+ interface LoadCostCharacterization {
62
62
  measuredOn: string;
63
63
  provenance: string;
64
64
  metric: 'lines';
65
65
  scopeSemantics: 'conditional' | 'full';
66
66
  }
67
- export interface ProjectionDiagnostic {
68
- readonly code: 'PROJECTION_INPUT_INVALID' | 'PROJECTION_GUIDE_UNAVAILABLE' | 'PROJECTION_GUIDE_CHANGED' | 'PROJECTION_GUIDE_ESCAPE' | 'PROJECTION_GUIDE_READ_FAILED' | 'PROJECTION_GUIDE_CLOSE_FAILED' | 'PROJECTION_ROOT_AMBIGUOUS' | 'PROJECTION_RESULT_BOUNDS' | 'PROJECTION_MOUNT_MISMATCH' | 'PROJECTION_GRANT_MISMATCH';
67
+ interface ProjectionDiagnostic {
68
+ readonly code: 'PROJECTION_INPUT_INVALID' | 'PROJECTION_GUIDE_UNAVAILABLE' | 'PROJECTION_GUIDE_CHANGED' | 'PROJECTION_GUIDE_ESCAPE' | 'PROJECTION_GUIDE_SECRET' | 'PROJECTION_GUIDE_READ_FAILED' | 'PROJECTION_GUIDE_CLOSE_FAILED' | 'PROJECTION_ROOT_AMBIGUOUS' | 'PROJECTION_RESULT_BOUNDS' | 'PROJECTION_MOUNT_MISMATCH' | 'PROJECTION_GRANT_MISMATCH';
69
69
  readonly message: string;
70
70
  readonly target?: string;
71
71
  readonly expected?: string;
72
72
  readonly actual?: string;
73
73
  }
74
74
  /** A sibling guide copied into the region of the root AGENTS.md that cockpit owns. */
75
- export interface ProjectedGuide {
75
+ interface ProjectedGuide {
76
76
  readonly sibling: string;
77
77
  /** The guide's path inside its sibling, as declared. */
78
78
  readonly guide: string;
@@ -82,7 +82,7 @@ export interface ProjectedGuide {
82
82
  /** The block written into the root AGENTS.md: a heading, a provenance marker and the guide's text. */
83
83
  readonly bytes: string;
84
84
  }
85
- export interface ShippedInstructionCapability {
85
+ interface ShippedInstructionCapability {
86
86
  readonly target: SurfaceTarget;
87
87
  readonly mechanism: 'root-agents-guide' | 'rulesync-rules';
88
88
  readonly capability: 'instructed' | 'generated';
@@ -6,6 +6,7 @@ import { SURFACE_TARGETS } from './contract.js';
6
6
  import { GUARD_SURFACE_TARGETS as INSTRUCTION_TARGETS, HARNESS_REGISTRY } from './harness-registry.js';
7
7
  import { INSTRUCTION_POINTER_MARKERS } from '../environment/instruction-pointer.js';
8
8
  import { exactKeys } from '../exact-keys.js';
9
+ import { foldGuardSpelling } from '../guards/evaluator.js';
9
10
  const GUIDE_BYTE_LIMIT = 65_536;
10
11
  const GUIDE_COUNT_LIMIT = 256;
11
12
  const OBSERVATION_CONTENT_BYTE_LIMIT = 786_432;
@@ -51,15 +52,32 @@ function contained(root, path) {
51
52
  const fromRoot = relative(root, path);
52
53
  return fromRoot === '' || !fromRoot.startsWith('..') && !isAbsolute(fromRoot);
53
54
  }
55
+ // A guide is copied into every agent's instructions, so it never reaches what the guard keeps out of
56
+ // their context: a `.env` or `.env.*` file, anything below a `.secrets` directory, or a declared
57
+ // environment instance. `fromRoot` is the path inside its sibling root; `instances` are folded.
58
+ function secretGuide(fromRoot, path, instances) {
59
+ const segments = foldGuardSpelling(fromRoot).split('/');
60
+ const name = segments.at(-1);
61
+ if (instances.has(foldGuardSpelling(path)))
62
+ return 'a declared environment instance';
63
+ if (segments.includes('.secrets'))
64
+ return 'below a .secrets directory';
65
+ if (name === '.env' || name.startsWith('.env.'))
66
+ return 'an environment file';
67
+ return undefined;
68
+ }
54
69
  function snapshotRequests(input) {
55
70
  const snapshot = snapshotJson(input);
56
- if (!isObject(snapshot) || !exactKeys(snapshot, ['allowedRoots', 'requests']) || !Array.isArray(snapshot.requests)
71
+ if (!isObject(snapshot) || !exactKeys(snapshot, ['allowedRoots', 'requests'], ['instances']) || !Array.isArray(snapshot.requests)
57
72
  || snapshot.requests.length > GUIDE_COUNT_LIMIT)
58
73
  return undefined;
59
74
  const allowedRoots = stringArray(snapshot.allowedRoots);
60
75
  if (!allowedRoots || allowedRoots.length === 0 || allowedRoots.length > GUIDE_COUNT_LIMIT
61
76
  || new Set(allowedRoots).size !== allowedRoots.length || allowedRoots.some(root => !isAbsolute(root)))
62
77
  return undefined;
78
+ const instances = snapshot.instances === undefined ? [] : stringArray(snapshot.instances);
79
+ if (!instances || instances.length > GUIDE_COUNT_LIMIT || instances.some(path => !isAbsolute(path)))
80
+ return undefined;
63
81
  const result = [];
64
82
  const identities = new Set();
65
83
  for (const request of snapshot.requests) {
@@ -77,7 +95,7 @@ function snapshotRequests(input) {
77
95
  identities.add(identity);
78
96
  result.push({ slug: request.slug, root: request.root, sourcePath });
79
97
  }
80
- return { requests: result, allowedRoots };
98
+ return { requests: result, allowedRoots, instances };
81
99
  }
82
100
  function statIdentity(stat) {
83
101
  return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
@@ -168,7 +186,15 @@ async function pathIdentity(sourcePath) {
168
186
  const target = await lstat(contentPath, { bigint: true });
169
187
  return { entry, entryType, ...(linkTarget === undefined ? {} : { linkTarget }), contentPath, target };
170
188
  }
171
- async function readGuide(request, requestRoot) {
189
+ function secretGuideDiagnostic(request, reason, resolved) {
190
+ return diagnostic('PROJECTION_GUIDE_SECRET', `guide ${request.sourcePath} ${resolved === undefined ? 'is' : `resolves to ${resolved},`} ${reason}, `
191
+ + 'which the guard keeps out of agent context, and sync would copy it into the root AGENTS.md; '
192
+ + `${resolved === undefined ? '' : 'replace the link with a file of guidance, or '}remove it from the guides of sibling ${request.slug} in cockpit.toml`, { target: request.sourcePath });
193
+ }
194
+ async function readGuide(request, requestRoot, instances) {
195
+ const named = secretGuide(relative(request.root, request.sourcePath), request.sourcePath, instances);
196
+ if (named)
197
+ return secretGuideDiagnostic(request, named);
172
198
  if (!await rootAuthorityStable(requestRoot)) {
173
199
  return diagnostic('PROJECTION_GUIDE_CHANGED', `declared sibling root changed before guide observation: ${request.root}`, { target: request.root });
174
200
  }
@@ -189,6 +215,10 @@ async function readGuide(request, requestRoot) {
189
215
  target: request.sourcePath, expected: requestRoot.canonical, actual: pathBefore.contentPath,
190
216
  });
191
217
  }
218
+ // A link spelled like a guide reaches its target's bytes.
219
+ const resolved = secretGuide(relative(requestRoot.canonical, pathBefore.contentPath), pathBefore.contentPath, instances);
220
+ if (resolved)
221
+ return secretGuideDiagnostic(request, resolved, pathBefore.contentPath);
192
222
  let handle;
193
223
  let result;
194
224
  try {
@@ -307,6 +337,11 @@ export async function observeSiblingGuides(input) {
307
337
  if (new Set(allowedRoots.map(root => root.canonical)).size !== allowedRoots.length) {
308
338
  return finishRootObservation(allowedRoots, observedOutcome({ kind: 'refusal', diagnostic: diagnostic('PROJECTION_ROOT_AMBIGUOUS', 'declared sibling roots resolve to the same canonical authority') }));
309
339
  }
340
+ // Each instance under every spelling a guide can reach it by: as declared, and below its root's real path.
341
+ const instances = new Set(captured.instances.flatMap(path => {
342
+ const root = allowedRoots.find(candidate => contained(candidate.declared, path));
343
+ return root ? [path, join(root.canonical, relative(root.declared, path))] : [path];
344
+ }).map(foldGuardSpelling));
310
345
  const observations = [];
311
346
  const missing = [];
312
347
  let contentBytes = 0;
@@ -321,11 +356,11 @@ export async function observeSiblingGuides(input) {
321
356
  return finishRootObservation(allowedRoots, observedOutcome({ kind: 'refusal', diagnostic: diagnostic('PROJECTION_INPUT_INVALID', `guide request is not bound to its most-specific sibling root: ${request.sourcePath}`, { target: request.sourcePath }) }));
322
357
  }
323
358
  const requestRoot = allowedRoots.find(root => root.declared === request.root);
324
- const observed = await readGuide(request, requestRoot);
359
+ const observed = await readGuide(request, requestRoot, instances);
325
360
  if (observed === 'missing')
326
361
  missing.push(request.sourcePath);
327
362
  else if ('code' in observed) {
328
- const refusal = ['PROJECTION_GUIDE_CHANGED', 'PROJECTION_GUIDE_ESCAPE', 'PROJECTION_GUIDE_READ_FAILED',
363
+ const refusal = ['PROJECTION_GUIDE_CHANGED', 'PROJECTION_GUIDE_ESCAPE', 'PROJECTION_GUIDE_SECRET', 'PROJECTION_GUIDE_READ_FAILED',
329
364
  'PROJECTION_GUIDE_CLOSE_FAILED', 'PROJECTION_RESULT_BOUNDS'].includes(observed.code);
330
365
  return finishRootObservation(allowedRoots, observedOutcome({ kind: refusal ? 'refusal' : 'unavailable', diagnostic: observed }));
331
366
  }
@@ -365,7 +400,7 @@ function capturePlannerInput(input) {
365
400
  return undefined;
366
401
  const activeProfiles = stringArray(value.activeProfiles);
367
402
  if (!activeProfiles || new Set(activeProfiles).size !== activeProfiles.length
368
- || activeProfiles.some(profile => !/^[a-z0-9][a-z0-9.-]*$/.test(profile)))
403
+ || activeProfiles.some(profile => !/^[a-z0-9][a-z0-9_.-]*$/.test(profile)))
369
404
  return undefined;
370
405
  const siblings = [];
371
406
  const siblingSlugs = new Set();
@@ -1,10 +1,5 @@
1
- import { type CustomHarness as CustomHarnessValue } from '../schema.js';
2
1
  import { type SurfaceFeature, type SurfaceTarget } from './contract.js';
3
- import { type SurfaceGenerationOutcome, type SurfaceGenerationWarning } from './generator-adapter.js';
4
- import type { SurfaceMergeResult } from './merge.js';
5
- import type { McpCollectionOutcome } from './mcp.js';
6
- import type { ProjectionPlanResult } from './projection.js';
7
- import type { RestrictionExpansionResult } from './restrictions.js';
2
+ import { type SurfaceGenerationWarning } from './generator-adapter.js';
8
3
  import { type SurfaceDesiredEntry, type SurfaceDiagnostic } from './ownership.js';
9
4
  export type SurfaceInputEvidence = Readonly<{
10
5
  kind: 'guide';
@@ -29,7 +24,7 @@ export type SurfaceInputEvidence = Readonly<{
29
24
  sourceSha256: string;
30
25
  resultSha256: string;
31
26
  }>;
32
- export interface NativeSurfaceFragment {
27
+ interface NativeSurfaceFragment {
33
28
  readonly target: SurfaceTarget;
34
29
  readonly path: string;
35
30
  readonly baseSha256: string;
@@ -52,7 +47,7 @@ export interface CustomSurfaceReceipt {
52
47
  scope: 'always' | 'scoped';
53
48
  }>[];
54
49
  }
55
- export type SurfaceRenderResult = Readonly<{
50
+ type SurfaceRenderResult = Readonly<{
56
51
  kind: 'complete';
57
52
  entries: readonly SurfaceDesiredEntry[];
58
53
  inputs: readonly SurfaceInputEvidence[];
@@ -65,43 +60,8 @@ export interface NativeFragmentSet {
65
60
  readonly modelSha256: string;
66
61
  readonly fragments: readonly NativeSurfaceFragment[];
67
62
  }
68
- /** A reference file pulled on demand by one shipped skill; rendered beside that skill's entry point on every built-in harness. */
69
- export interface SkillReference {
70
- readonly skill: string;
71
- readonly path: string;
72
- readonly bytes: string;
73
- }
74
- export interface SurfaceRenderInput {
75
- readonly configuredHarnesses: readonly string[];
76
- readonly merge: Extract<SurfaceMergeResult, {
77
- kind: 'complete';
78
- }>;
79
- readonly generated: Extract<SurfaceGenerationOutcome, {
80
- kind: 'complete';
81
- }>;
82
- readonly projection: Extract<ProjectionPlanResult, {
83
- kind: 'complete';
84
- }>;
85
- readonly mcp: Extract<McpCollectionOutcome, {
86
- kind: 'complete';
87
- }>;
88
- readonly restrictions: Extract<RestrictionExpansionResult, {
89
- kind: 'complete';
90
- }>;
91
- readonly nativeMcp: NativeFragmentSet;
92
- readonly nativeGrants: NativeFragmentSet;
93
- readonly browser: unknown | null;
94
- readonly browserReference: string | null;
95
- readonly skillReferences: readonly SkillReference[];
96
- readonly testing: unknown | null;
97
- readonly customHarnesses: readonly CustomHarnessValue[];
98
- readonly customReceipts: readonly CustomSurfaceReceipt[];
99
- readonly sourceEvidence: readonly Extract<SurfaceInputEvidence, {
100
- kind: 'guide' | 'mcp';
101
- }>[];
102
- }
103
63
  export declare function surfaceModelSha256(value: unknown): string | undefined;
104
- export type HookSettingsComposition = Readonly<{
64
+ type HookSettingsComposition = Readonly<{
105
65
  kind: 'complete';
106
66
  value: Record<string, unknown>;
107
67
  }> | Readonly<{
@@ -116,3 +76,4 @@ export type HookSettingsComposition = Readonly<{
116
76
  */
117
77
  export declare function composeHookSettings(values: readonly unknown[]): HookSettingsComposition;
118
78
  export declare function renderSurfaceSet(input: unknown): SurfaceRenderResult;
79
+ export {};
@@ -364,8 +364,10 @@ export function renderSurfaceSet(input) {
364
364
  if (harness.servers.length > 0)
365
365
  mcpTargets.add(harness.target);
366
366
  }
367
- if (captured.mcp.result.servers.length > 0 && mcpTargets.size === 0)
367
+ // A remote server may fit none of the listed tools, and says so; a local one fits every tool.
368
+ if (captured.mcp.result.servers.some(server => object(server) && server.transport !== 'http') && mcpTargets.size === 0) {
368
369
  return refuse('SURFACE_INPUT_INVALID', 'MCP model has no matching native transport');
370
+ }
369
371
  const mcpModelDigest = surfaceModelSha256(captured.mcp.result);
370
372
  if (!mcpModelDigest)
371
373
  return refuse('SURFACE_BOUNDS', 'MCP model cannot be captured for native binding');
@@ -475,8 +477,9 @@ export function renderSurfaceSet(input) {
475
477
  for (const wrapper of captured.mcp.result.wrappers) {
476
478
  if (!validFile(wrapper))
477
479
  return refuse('SURFACE_INPUT_INVALID', 'MCP wrapper output is malformed');
480
+ // A local server's wrapper starts it; a remote server's wrapper runs its headers helper.
478
481
  const server = captured.mcp.result.servers.find(row => object(row) && typeof row.name === 'string'
479
- && wrapper.path === `.cockpit/mcp/${sha256Hex(row.name)}.sh`);
482
+ && wrapper.path === `.cockpit/mcp/${sha256Hex(row.name)}${row.transport === 'http' ? '.headers' : ''}.sh`);
480
483
  const harnesses = object(server) ? captured.mcp.result.harnesses.filter(row => object(row)
481
484
  && Array.isArray(row.servers) && row.servers.some(transport => object(transport) && transport.name === server.name))
482
485
  .map(row => row.target) : [];
@@ -253,7 +253,8 @@ async function ignoreBlock(root, config) {
253
253
  const siblingsRoot = config.cockpit.siblings_root;
254
254
  if (/[\0\r\n]/.test(siblingsRoot))
255
255
  return undefined;
256
- const patterns = ['/node_modules/', '/.cockpit/local/', '/env/.secrets/*', '!/env/.secrets/.gitignore'];
256
+ // Claude Code's machine-local settings, where `cockpit use` grants a served checkout.
257
+ const patterns = ['/node_modules/', '/.cockpit/local/', '/.claude/settings.local.json', '/env/.secrets/*', '!/env/.secrets/.gitignore'];
257
258
  const inside = (path, kind = 'directory') => {
258
259
  const local = relative(root, path);
259
260
  if (local === '' || local === '..' || local.startsWith(`..${sep}`) || isAbsolute(local))
@@ -0,0 +1,34 @@
1
+ export interface DiffFile {
2
+ old: string;
3
+ next: string;
4
+ hunk: boolean;
5
+ removed: boolean;
6
+ removedLines: string[];
7
+ added: string[];
8
+ renamed: boolean;
9
+ deleted: boolean;
10
+ }
11
+ export interface WeakeningFinding {
12
+ readonly path: string;
13
+ readonly kind: WeakeningKind;
14
+ readonly reason: string;
15
+ }
16
+ type WeakeningKind = 'test-file-removed' | 'test-skipped' | 'check-suppressed' | 'tests-removed' | 'assertions-removed' | 'assertions-loosened' | 'assertion-vacuous' | 'test-selection-narrowed';
17
+ /** Reads `git diff` output (taken with --unified=0 and core.quotePath=true) into one record per file. */
18
+ export declare function parseDiff(diff: string): DiffFile[];
19
+ /**
20
+ * Why added lines turn off or short-circuit what runs. Only explicit added-line forms are measured;
21
+ * this is not JavaScript dataflow analysis.
22
+ */
23
+ export declare function additiveBypassReasons(added: readonly string[]): string[];
24
+ export interface TestRoles {
25
+ readonly isTest: (path: string) => boolean;
26
+ /** A runner's configuration, or a manifest that carries a test script. */
27
+ readonly isTestConfig: (path: string) => boolean;
28
+ }
29
+ /**
30
+ * The findings a diff earns over the files `roles` calls tests. A diff cannot prove that a
31
+ * replacement keeps its force, so a removal is flagged and a person acknowledges the legitimate ones.
32
+ */
33
+ export declare function classifyTestWeakening(files: readonly DiffFile[], roles: TestRoles): WeakeningFinding[];
34
+ export {};
@@ -0,0 +1,137 @@
1
+ // Detects a change that makes tests pass by weakening them. It has no imports on purpose: CI runs
2
+ // the base revision's copy of scripts/check-test-protection.mjs beside this file, so the detector
3
+ // that judges a change is never the one the change edited, and the same code judges a consumer's
4
+ // checkout when `work grade` runs.
5
+ function gitPath(value) {
6
+ if (!value.startsWith('"'))
7
+ return value;
8
+ // Git quotes paths as C strings, including octal UTF-8 bytes.
9
+ const bytes = [];
10
+ for (let i = 1; i < value.length - 1; i++) {
11
+ if (value[i] !== '\\') {
12
+ bytes.push(...Buffer.from(value[i]));
13
+ continue;
14
+ }
15
+ const octal = /^[0-7]{1,3}/.exec(value.slice(i + 1))?.[0];
16
+ if (octal) {
17
+ bytes.push(Number.parseInt(octal, 8));
18
+ i += octal.length;
19
+ continue;
20
+ }
21
+ const escaped = value[++i];
22
+ const controls = { t: '\t', n: '\n', r: '\r', b: '\b', f: '\f', v: '\v', '\\': '\\', '"': '"' };
23
+ if (!(escaped in controls))
24
+ throw new Error('unsupported Git path escape');
25
+ bytes.push(...Buffer.from(controls[escaped]));
26
+ }
27
+ return Buffer.from(bytes).toString('utf8');
28
+ }
29
+ /** Reads `git diff` output (taken with --unified=0 and core.quotePath=true) into one record per file. */
30
+ export function parseDiff(diff) {
31
+ const files = [];
32
+ let file;
33
+ for (const line of diff.split('\n')) {
34
+ if (line.startsWith('diff --git ')) {
35
+ const names = /^diff --git ("(?:\\.|[^"])*"|a\/\S+) ("(?:\\.|[^"])*"|b\/\S+)$/.exec(line);
36
+ file = { old: names ? gitPath(names[1]).slice(2) : '', next: names ? gitPath(names[2]).slice(2) : '', hunk: false, removed: false, removedLines: [], renamed: false, deleted: false, added: [] };
37
+ files.push(file);
38
+ }
39
+ else if (file) {
40
+ if (line.startsWith('@@ '))
41
+ file.hunk = true;
42
+ else if (!file.hunk && line.startsWith('--- '))
43
+ file.old = gitPath(line.slice(4)).replace(/^a\//, '');
44
+ else if (!file.hunk && line.startsWith('+++ '))
45
+ file.next = gitPath(line.slice(4)).replace(/^b\//, '');
46
+ else if (line.startsWith('rename from ')) {
47
+ file.old = gitPath(line.slice(12));
48
+ file.renamed = true;
49
+ }
50
+ else if (line.startsWith('rename to '))
51
+ file.next = gitPath(line.slice(10));
52
+ else if (line.startsWith('deleted file mode '))
53
+ file.deleted = true;
54
+ else if (line.startsWith('-') && line.slice(1).trim()) {
55
+ file.removed = true;
56
+ file.removedLines.push(line.slice(1));
57
+ }
58
+ else if (line.startsWith('+'))
59
+ file.added.push(line.slice(1));
60
+ else if (line.startsWith('Binary files '))
61
+ file.removed = true;
62
+ }
63
+ }
64
+ return files;
65
+ }
66
+ /**
67
+ * Why added lines turn off or short-circuit what runs. Only explicit added-line forms are measured;
68
+ * this is not JavaScript dataflow analysis.
69
+ */
70
+ export function additiveBypassReasons(added) {
71
+ const reasons = [];
72
+ if (/(?:\.\s*|\[\s*['"])(?:skip|todo|only|skipIf|runIf|fails)\b|\b(?:skip|todo|only)\s*:\s*true/.test(added.join('\n'))) {
73
+ reasons.push('test selection or expected-failure semantics added');
74
+ }
75
+ if (added.some(line => /^[ \t]*return[ \t]*;?[ \t]*(?:\/\/.*)?$/.test(line))) {
76
+ reasons.push('potential additive control-flow bypass: bare return added');
77
+ }
78
+ if (added.some(line => /^[ \t]*(?:if\s*\(\s*(?:true|1)\s*\)\s*(?:\{\s*)?return\s*;?(?:\s*\})?\s*(?:\/\/.*)?$|if\s*\(\s*(?:false|0|null|undefined)\s*\)\s*\{|(?:false\s*&&|true\s*\|\|)\s*(?:describe|test|it|suite)\s*\(|process\s*\.\s*exit\s*\(\s*(?:0\s*)?\))/.test(line))) {
79
+ reasons.push('potential additive control-flow bypass: explicit static bypass added');
80
+ }
81
+ return reasons;
82
+ }
83
+ // JS/TS first, then the Python forms that mean the same. A match in an added comment still counts:
84
+ // a directive is a comment.
85
+ const SKIPPED = /\b(?:it|test|describe|suite|context|bench)\s*\.\s*(?:skip|only|todo|skipIf|runIf|fails|failing|fixme)\b|\b(?:xit|xtest|xdescribe|xcontext|fit|fdescribe)\s*\(|\b(?:skip|only)\s*:\s*true|\bpytest\s*\.\s*(?:mark\s*\.\s*(?:skip|skipif|xfail)|skip)\b|\bunittest\s*\.\s*(?:skip|skipIf|skipUnless|expectedFailure)\b|^\s*@(?:skip|skipIf|skipUnless|expectedFailure)\b|\bself\s*\.\s*skipTest\s*\(/;
86
+ const SUPPRESSED = /@ts-ignore|@ts-nocheck|eslint-disable|\bnoqa\b|\btype:\s*ignore\b|\bpylint:\s*disable\b/;
87
+ const TEST_DECLARATION = /^\s*(?:(?:it|test)\s*(?:\.\s*\w+)*\s*(?:\.\s*each\s*\([^)]*\))?\s*\(|def\s+test_|async\s+def\s+test_)/;
88
+ const ASSERTION = /\bexpect\s*\(|\bassert\b|\bassert[A-Z]\w*\s*\(|\.should\b|\bt\.(?:is|true|deepEqual|equal|throws)\s*\(/;
89
+ const STRONG = /\.(?:toBe|toEqual|toStrictEqual|toMatchObject|toHaveBeenCalledWith|toHaveLength|toThrow\w*|toContain\w*|toMatch)\s*\(|\bassert(?:Equal|Equals|In|Raises|IsNone)\b|\bassert\s+\S+\s*(?:==|!=|in)\s/;
90
+ const WEAK = /\.(?:toBeDefined|toBeTruthy|toBeFalsy|toBeUndefined)\s*\(|\bexpect\.(?:anything|any)\s*\(|\.not\.toBeNull\s*\(|\bassert\s+\w+\s*$|\bassertTrue\s*\(\s*\w+\s*\)/;
91
+ const VACUOUS = /\bexpect\s*\(\s*(?:true|1|false|0|null|'[^']*'|"[^"]*")\s*\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\s*\(|\bassert\s+(?:True|1|not\s+False)\s*$|\bassert\s*\(?\s*true\s*\)?\s*;?\s*$|\bassertTrue\s*\(\s*True\s*\)/;
92
+ // What a test runner's own configuration says about which files it runs.
93
+ const NARROWING = /\b(?:exclude|testPathIgnorePatterns|testIgnore|ignore|deselect|norecursedirs|skip)\b|--ignore\b|--deselect\b|--exclude\b|--passWithNoTests|\|\|\s*true\b|\bexit\s+0\b/;
94
+ const WIDENING = /\b(?:include|testMatch|testRegex|testpaths|projects)\b/;
95
+ const TEST_SCRIPT = /^\s*"(?:pre|post)?test[^"]*"\s*:/;
96
+ const count = (lines, pattern) => lines.filter(line => pattern.test(line)).length;
97
+ /**
98
+ * The findings a diff earns over the files `roles` calls tests. A diff cannot prove that a
99
+ * replacement keeps its force, so a removal is flagged and a person acknowledges the legitimate ones.
100
+ */
101
+ export function classifyTestWeakening(files, roles) {
102
+ const findings = [];
103
+ for (const file of files) {
104
+ const path = file.deleted || file.next === '/dev/null' ? file.old : file.next;
105
+ const finding = (kind, reason) => findings.push({ path, kind, reason });
106
+ const wasTest = roles.isTest(file.old);
107
+ if (wasTest && (file.deleted || file.next === '/dev/null' || file.renamed && !roles.isTest(file.next))) {
108
+ finding('test-file-removed', 'a test file was deleted or moved out of the tests');
109
+ continue;
110
+ }
111
+ if (roles.isTest(path)) {
112
+ if (count(file.added, SKIPPED))
113
+ finding('test-skipped', 'a test was skipped, focused, marked todo or expected to fail');
114
+ if (count(file.added, SUPPRESSED))
115
+ finding('check-suppressed', 'a type or lint check was suppressed in a test');
116
+ if (count(file.removedLines, TEST_DECLARATION) > count(file.added, TEST_DECLARATION))
117
+ finding('tests-removed', 'more tests were removed than added');
118
+ if (count(file.removedLines, ASSERTION) > count(file.added, ASSERTION))
119
+ finding('assertions-removed', 'more assertions were removed than added');
120
+ else if (count(file.removedLines, STRONG) > count(file.added, STRONG) && count(file.added, WEAK) > count(file.removedLines, WEAK)) {
121
+ finding('assertions-loosened', 'specific assertions were replaced by weaker ones');
122
+ }
123
+ if (count(file.added, VACUOUS))
124
+ finding('assertion-vacuous', 'an assertion that cannot fail was added');
125
+ }
126
+ else if (roles.isTestConfig(path)) {
127
+ const manifest = /(?:^|\/)package\.json$/.test(path);
128
+ const scriptsAdded = manifest ? file.added.filter(line => TEST_SCRIPT.test(line)) : file.added;
129
+ if (count(scriptsAdded, NARROWING))
130
+ finding('test-selection-narrowed', 'test configuration now excludes, ignores or tolerates the absence of tests');
131
+ else if (manifest ? count(file.removedLines, TEST_SCRIPT) > count(scriptsAdded, TEST_SCRIPT)
132
+ : count(file.removedLines, WIDENING) > count(file.added, WIDENING))
133
+ finding('test-selection-narrowed', 'test configuration no longer selects what it selected');
134
+ }
135
+ }
136
+ return findings;
137
+ }
@@ -0,0 +1,15 @@
1
+ import { type DiffFile, type WeakeningFinding } from './test-protection.js';
2
+ export interface TestFilePatterns {
3
+ readonly tests: readonly string[];
4
+ readonly configuration: readonly string[];
5
+ /** Where the test-file patterns came from, stated beside every finding. */
6
+ readonly source: string;
7
+ }
8
+ export declare function testFilePatterns(declared: readonly string[] | undefined): TestFilePatterns;
9
+ export declare function weakeningFindings(files: readonly DiffFile[], patterns: TestFilePatterns): WeakeningFinding[];
10
+ export interface RepositoryWeakening extends WeakeningFinding {
11
+ readonly repository: string;
12
+ }
13
+ /** The digest an acknowledgement binds: exactly these findings, and no others, are acknowledged. */
14
+ export declare function testWeakeningDigest(findings: readonly RepositoryWeakening[]): string;
15
+ export declare const describeWeakening: (row: RepositoryWeakening) => string;
@@ -0,0 +1,30 @@
1
+ // Owns which files a checkout treats as tests and how a finding is identified: the patterns a
2
+ // consumer declared under [testing] test_files or, failing that, the common layouts, plus the digest
3
+ // a person's acknowledgement binds to. The detection itself is test-protection.ts.
4
+ import { sha256Hex } from '../sha256.js';
5
+ import { scopeMatcher } from '../work-orders/contract.js';
6
+ import { classifyTestWeakening } from './test-protection.js';
7
+ const DEFAULT_TEST_FILES = Object.freeze([
8
+ '**/*.test.*', '**/*.spec.*', '**/__tests__/**', '**/tests/**', '**/test/**', '**/e2e/**',
9
+ '**/test_*.py', '**/*_test.py', '**/conftest.py', '**/*_test.go', '**/*Test.java', '**/*_spec.rb',
10
+ ]);
11
+ // What decides which tests run; a declared test_files list does not replace these.
12
+ const TEST_CONFIGURATION = Object.freeze([
13
+ '**/vitest.config.*', '**/vitest.workspace.*', '**/jest.config.*', '**/playwright.config.*', '**/.mocharc*',
14
+ '**/pytest.ini', '**/tox.ini', '**/package.json',
15
+ ]);
16
+ export function testFilePatterns(declared) {
17
+ return declared?.length
18
+ ? { tests: declared, configuration: TEST_CONFIGURATION, source: '[testing] test_files in cockpit.toml' }
19
+ : { tests: DEFAULT_TEST_FILES, configuration: TEST_CONFIGURATION, source: 'the built-in test-file patterns for common layouts (no [testing] test_files declared)' };
20
+ }
21
+ export function weakeningFindings(files, patterns) {
22
+ const roles = { isTest: scopeMatcher(patterns.tests), isTestConfig: scopeMatcher(patterns.configuration) };
23
+ return classifyTestWeakening(files, roles);
24
+ }
25
+ /** The digest an acknowledgement binds: exactly these findings, and no others, are acknowledged. */
26
+ export function testWeakeningDigest(findings) {
27
+ const rows = findings.map(row => `${row.repository}\0${row.path}\0${row.kind}`).sort();
28
+ return sha256Hex(JSON.stringify(rows));
29
+ }
30
+ export const describeWeakening = (row) => `${row.repository === '.' ? row.path : `${row.repository}:${row.path}`}: ${row.kind} (${row.reason})`;
@@ -0,0 +1,7 @@
1
+ import type { WorkRecord } from './store.js';
2
+ import { type WorkApplicationOutcome, type WorkContext, type WorkRequest } from './application-context.js';
3
+ export declare const pendingCi: (record: WorkRecord) => string[];
4
+ export declare function signoff(request: Extract<WorkRequest, {
5
+ operation: 'work-signoff';
6
+ }>, context: WorkContext): Promise<WorkApplicationOutcome>;
7
+ export declare function verify(context: WorkContext): Promise<WorkApplicationOutcome>;
@@ -0,0 +1,76 @@
1
+ // How an order closes: `work signoff` closes it as done, partial or abandoned, and `work verify`, the CI
2
+ // job, checks every closure and confirms the steps that awaited CI.
3
+ import { runningInCi } from '../autonomy.js';
4
+ import { RefusalError } from '../environment/contract.js';
5
+ import { complete, now, refusal, TERMINAL, unsatisfied } from './application-context.js';
6
+ import { decisions, recordProblem, unresolvedDecisions } from './application-decisions.js';
7
+ const CLOSURE = Object.freeze({ independent: 'graded', checks: 'checks-passed', self: 'self-reported' });
8
+ export const pendingCi = (record) => [
9
+ ...record.acknowledgements.filter(row => row.by === 'ci').map(() => 'acknowledge'),
10
+ ...record.accepted.by === 'ci' ? ['accept'] : [], ...record.amendments.filter(row => row.by === 'ci').map(() => 'amend'),
11
+ ...record.notes.filter(row => row.kind === 'answer' && row.by === 'ci').map(() => 'answer'), ...record.closure?.by === 'ci' ? ['signoff'] : [],
12
+ ];
13
+ export async function signoff(request, context) {
14
+ const { composition, operation, policy, store, current, gated } = context;
15
+ const record = await store.read(request.workOrderId);
16
+ if (!record)
17
+ return refusal(operation, `${request.workOrderId} is not accepted`);
18
+ if (TERMINAL.has(record.state))
19
+ return refusal(operation, `${request.workOrderId} is already closed (${record.state})`);
20
+ let how;
21
+ if (request.close === 'done') {
22
+ const { order } = await current(request.workOrderId);
23
+ if (record.state !== 'graded' || !record.grade?.passed || record.grade.digest !== record.accepted.digest) {
24
+ return refusal(operation, `${order.id} is ${record.state}, not graded; run npx --no cockpit work grade --config <your cockpit.toml> --work-order ${order.id}`);
25
+ }
26
+ const open = await Promise.all(order.depends_on.map(async (dependency) => (await store.read(dependency))?.state === 'done' ? undefined : dependency));
27
+ if (open.some(Boolean))
28
+ return refusal(operation, `${order.id} depends on ${open.filter(Boolean).join(', ')}, which ${open.filter(Boolean).length === 1 ? 'is' : 'are'} not done`);
29
+ how = CLOSURE[record.grade.mode];
30
+ }
31
+ else {
32
+ if (!request.note)
33
+ return refusal(operation, `closing ${request.workOrderId} as ${request.close} needs --note saying what was and was not done`);
34
+ how = request.close;
35
+ }
36
+ // Every closure, partial and abandoned too, settles the order's decisions: work that stops still made them.
37
+ const open = await unresolvedDecisions(record, composition);
38
+ if (open.length)
39
+ return refusal(operation, `${request.workOrderId} cannot close while ${open.join('; ')}`);
40
+ // A grade found the agent's own amendments lowered the bar; a re-grade must not let it close the order unseen.
41
+ const failedReview = record.amendmentsFailedReview;
42
+ const decider = failedReview && policy.gates.signoff === 'agent' ? 'person' : policy.gates.signoff;
43
+ return gated('signoff', decider, [`Close ${request.workOrderId} as ${request.close} (${how})`, ...request.note ? [`Note: ${request.note}`] : [],
44
+ ...failedReview ? [`A grade found the agent's amendments lower the bar: ${failedReview.note}`] : [],
45
+ ...record.grade ? record.grade.criteria.map(row => `${row.id}: ${row.verdict} — ${row.note}`) : [],
46
+ ...decisions(record).map(row => `Decision ${row.decision} (${row.title}): ${row.resolution.record ? `recorded in ${row.resolution.record}` : `no lasting decision — ${row.resolution.text}`}`)], 'signoff', async (by) => store.update(request.workOrderId, async (existing) => {
47
+ if (!existing || existing.state !== record.state || existing.accepted.digest !== record.accepted.digest || existing.notes.length !== record.notes.length) {
48
+ throw new RefusalError(`${request.workOrderId} changed while this prompt was open`);
49
+ }
50
+ const state = request.close;
51
+ return { record: { ...existing, state, closure: { at: now(), by, how, ...request.note ? { note: request.note } : {} } },
52
+ value: complete(operation, { id: request.workOrderId, state, closedAs: how, by }) };
53
+ }));
54
+ }
55
+ export async function verify(context) {
56
+ const { composition, operation, store, observe } = context;
57
+ const problems = [];
58
+ const awaiting = [];
59
+ for (const record of await store.list()) {
60
+ const row = await observe(record.id);
61
+ if (!TERMINAL.has(record.state) && row.digest !== record.accepted.digest)
62
+ problems.push(`${record.id} changed since it was accepted; accept the change or revert it`);
63
+ if (record.state === 'done' && (!record.grade?.passed || record.grade.digest !== record.accepted.digest || !record.closure))
64
+ problems.push(`${record.id} is done without a passing grade of its accepted contract`);
65
+ for (const row of decisions(record)) {
66
+ const cited = row.resolution?.record;
67
+ const problem = cited && recordProblem(await composition.readDocsFile(cited), cited, record.id, false);
68
+ if (problem)
69
+ problems.push(`${record.id} decision ${row.decision}: ${problem}`);
70
+ }
71
+ awaiting.push(...pendingCi(record).map(step => `${record.id} ${step}`));
72
+ }
73
+ const ci = runningInCi(composition.env);
74
+ const value = { ci, problems, ...ci ? { confirmed: awaiting } : { awaitingCi: awaiting } };
75
+ return problems.length ? unsatisfied(operation, value, problems.join('; ')) : complete(operation, value);
76
+ }