kiokuko-dsh 0.2.26 → 0.2.27

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 (42) hide show
  1. package/dist/context/replay-model-memory.d.ts +2 -2
  2. package/dist/context/replay-model-memory.d.ts.map +1 -1
  3. package/dist/context/replay-model-memory.js +70 -16
  4. package/dist/context/replay-model-memory.js.map +1 -1
  5. package/dist/dsh/host-adapter/lifecycle.d.ts +20 -18
  6. package/dist/dsh/host-adapter/lifecycle.d.ts.map +1 -1
  7. package/dist/dsh/host-adapter/lifecycle.js +298 -102
  8. package/dist/dsh/host-adapter/lifecycle.js.map +1 -1
  9. package/dist/dsh/host-adapter.d.ts +47 -47
  10. package/dist/dsh/host-adapter.d.ts.map +1 -1
  11. package/dist/dsh/host-adapter.js +1213 -592
  12. package/dist/dsh/host-adapter.js.map +1 -1
  13. package/dist/dsh/index.d.ts +42 -41
  14. package/dist/dsh/index.d.ts.map +1 -1
  15. package/dist/dsh/index.js +166 -92
  16. package/dist/dsh/index.js.map +1 -1
  17. package/dist/dsh/lifecycle-memory.d.ts +11 -7
  18. package/dist/dsh/lifecycle-memory.d.ts.map +1 -1
  19. package/dist/dsh/lifecycle-memory.js +599 -172
  20. package/dist/dsh/lifecycle-memory.js.map +1 -1
  21. package/dist/dsh/task-completion.d.ts +7 -7
  22. package/dist/dsh/task-completion.d.ts.map +1 -1
  23. package/dist/dsh/task-completion.js +554 -194
  24. package/dist/dsh/task-completion.js.map +1 -1
  25. package/dist/dsh/task-state-host.d.ts +10 -10
  26. package/dist/dsh/task-state-host.d.ts.map +1 -1
  27. package/dist/dsh/task-state-host.js +337 -145
  28. package/dist/dsh/task-state-host.js.map +1 -1
  29. package/dist/index.js +1 -0
  30. package/dist/locale/en.json +1 -1
  31. package/dist/locale/ja.json +1 -1
  32. package/dist/locale/ko.json +1 -1
  33. package/dist/locale/zh-CN.json +1 -1
  34. package/dist/memory/replay-reflection.d.ts +94 -0
  35. package/dist/memory/replay-reflection.d.ts.map +1 -0
  36. package/dist/memory/replay-reflection.js +149 -0
  37. package/dist/memory/replay-reflection.js.map +1 -0
  38. package/dist/memory/replay-review.d.ts +18 -12
  39. package/dist/memory/replay-review.d.ts.map +1 -1
  40. package/dist/memory/replay-review.js +107 -46
  41. package/dist/memory/replay-review.js.map +1 -1
  42. package/package.json +2 -1
@@ -1,261 +1,464 @@
1
- import { assertionForCriterion, COMPLETION_BINDING_GUIDE } from './task-completion-guidance.js';
2
- import { ProtocolRecipeSchema, PROTOCOL_CAPABILITIES } from './guardrail/contracts.js';
3
- import { enrollProtocol, invalidateProtocolBinding, protocolCompletionReady, unresolvedManaged } from './guardrail/service.js';
4
- import { realpathSync, existsSync, readFileSync } from 'node:fs';
5
- import { createHash } from 'node:crypto';
6
- import path from 'node:path';
7
- import * as z from 'zod/v4';
8
- import { withImmediateTransaction } from '../db/transaction.js';
9
- import { KiokukoError } from '../errors.js';
10
- import { applicationSourceDigest } from '../memory/application.js';
11
- import { canonicalContentHash } from '../serialization/validate.js';
12
- import { completionVerifiers } from './completion-verifiers.js';
13
- import { readExecutionFrame } from './execution-frame.js';
14
- import { hasExplicitTestCases, parseNodeTapSummary, parseTestSummary, selectedNodeTestCommand } from './node-tap-summary.js';
15
- import { isHostExecutionResult } from './execution-result.js';
16
- import { executionCommand, EXECUTION_COMMAND_VERSION, hasShellPipeline } from './execution-command.js';
17
- import { readCodingWorkflow } from './coding-workflow/store.js';
18
- import { recordCodingAttempt } from './coding-workflow/service.js';
19
- export const CompletionConfig = z.object({ mode: z.enum(['shadow', 'enforce']).default('shadow') }).strict();
1
+ import { assertionForCriterion, COMPLETION_BINDING_GUIDE, } from "./task-completion-guidance.js";
2
+ import { ProtocolRecipeSchema, PROTOCOL_CAPABILITIES, } from "./guardrail/contracts.js";
3
+ import { enrollProtocol, invalidateProtocolBinding, protocolCompletionReady, unresolvedManaged, } from "./guardrail/service.js";
4
+ import { realpathSync, existsSync, readFileSync } from "node:fs";
5
+ import { createHash } from "node:crypto";
6
+ import path from "node:path";
7
+ import * as z from "zod/v4";
8
+ import { withImmediateTransaction } from "../db/transaction.js";
9
+ import { KiokukoError } from "../errors.js";
10
+ import { applicationSourceDigest } from "../memory/application.js";
11
+ import { canonicalContentHash } from "../serialization/validate.js";
12
+ import { completionVerifiers } from "./completion-verifiers.js";
13
+ import { readExecutionFrame } from "./execution-frame.js";
14
+ import { hasExplicitTestCases, parseNodeTapSummary, parseTestSummary, selectedNodeTestCommand, } from "./node-tap-summary.js";
15
+ import { isHostExecutionResult } from "./execution-result.js";
16
+ import { executionCommand, EXECUTION_COMMAND_VERSION, hasShellPipeline, } from "./execution-command.js";
17
+ import { readCodingWorkflow } from "./coding-workflow/store.js";
18
+ import { recordCodingAttempt } from "./coding-workflow/service.js";
19
+ export const CompletionConfig = z
20
+ .object({ mode: z.enum(["shadow", "enforce"]).default("shadow") })
21
+ .strict();
20
22
  const text = z.string().trim().min(1).max(4096);
21
- const relativePath = z.string().min(1).max(512).refine(value => !path.isAbsolute(value)
22
- && !value.split(/[\\/]/u).includes('..') && !/[\p{Cc}\p{Cf}]/u.test(value));
23
- export const CompletionMethodSchema = z.discriminatedUnion('kind', [
24
- z.object({ kind: z.literal('native_command'), command: text, cwd: text,
25
- sourcePaths: z.array(relativePath).min(1).max(32), assertion: z.enum(['exit_zero', 'selected_tests_pass', 'output_matches', 'artifact_matches']),
23
+ const relativePath = z
24
+ .string()
25
+ .min(1)
26
+ .max(512)
27
+ .refine((value) => !path.isAbsolute(value) &&
28
+ !value.split(/[\\/]/u).includes("..") &&
29
+ !/[\p{Cc}\p{Cf}]/u.test(value));
30
+ export const CompletionMethodSchema = z.discriminatedUnion("kind", [
31
+ z
32
+ .object({
33
+ kind: z.literal("native_command"),
34
+ command: text,
35
+ cwd: text,
36
+ sourcePaths: z.array(relativePath).min(1).max(32),
37
+ assertion: z.enum([
38
+ "exit_zero",
39
+ "selected_tests_pass",
40
+ "output_matches",
41
+ "artifact_matches",
42
+ ]),
26
43
  protocol: ProtocolRecipeSchema.optional(),
27
- expectedOutput: text.optional(), artifactPath: relativePath.optional(), artifactSha256: z.string().regex(/^[a-f0-9]{64}$/u).optional(),
28
- }).strict().superRefine((value, ctx) => {
29
- if (value.protocol && value.assertion !== 'selected_tests_pass')
30
- ctx.addIssue({ code: 'custom', path: ['assertion'], message: 'Protocol recipes require selected tests' });
31
- if (value.assertion === 'output_matches' && !value.expectedOutput)
32
- ctx.addIssue({ code: 'custom', path: ['expectedOutput'], message: 'Output assertion requires expectedOutput' });
33
- if (value.assertion === 'artifact_matches') {
44
+ expectedOutput: text.optional(),
45
+ artifactPath: relativePath.optional(),
46
+ artifactSha256: z
47
+ .string()
48
+ .regex(/^[a-f0-9]{64}$/u)
49
+ .optional(),
50
+ })
51
+ .strict()
52
+ .superRefine((value, ctx) => {
53
+ if (value.protocol && value.assertion !== "selected_tests_pass")
54
+ ctx.addIssue({
55
+ code: "custom",
56
+ path: ["assertion"],
57
+ message: "Protocol recipes require selected tests",
58
+ });
59
+ if (value.assertion === "output_matches" && !value.expectedOutput)
60
+ ctx.addIssue({
61
+ code: "custom",
62
+ path: ["expectedOutput"],
63
+ message: "Output assertion requires expectedOutput",
64
+ });
65
+ if (value.assertion === "artifact_matches") {
34
66
  if (!value.artifactPath)
35
- ctx.addIssue({ code: 'custom', path: ['artifactPath'], message: 'Artifact assertion requires path' });
67
+ ctx.addIssue({
68
+ code: "custom",
69
+ path: ["artifactPath"],
70
+ message: "Artifact assertion requires path",
71
+ });
36
72
  if (!value.artifactSha256)
37
- ctx.addIssue({ code: 'custom', path: ['artifactSha256'], message: 'Artifact assertion requires SHA-256' });
73
+ ctx.addIssue({
74
+ code: "custom",
75
+ path: ["artifactSha256"],
76
+ message: "Artifact assertion requires SHA-256",
77
+ });
38
78
  }
39
79
  }),
40
- z.object({ kind: z.literal('enno_verifier'), verifierId: text, workUnitId: text.optional(),
41
- assertion: z.enum(['exit_zero', 'selected_tests_pass']) }).strict(),
80
+ z
81
+ .object({
82
+ kind: z.literal("enno_verifier"),
83
+ verifierId: text,
84
+ workUnitId: text.optional(),
85
+ assertion: z.enum(["exit_zero", "selected_tests_pass"]),
86
+ })
87
+ .strict(),
42
88
  ]);
43
89
  export { parseNodeTapSummary, selectedNodeTestCommand };
44
90
  export function initializeTaskCompletion(database, runId, mode) {
45
- database.prepare('INSERT OR IGNORE INTO dsh_completion_runs(run_id,mode,created_at) VALUES(?,?,?)')
91
+ database
92
+ .prepare("INSERT OR IGNORE INTO dsh_completion_runs(run_id,mode,created_at) VALUES(?,?,?)")
46
93
  .run(runId, mode, new Date().toISOString());
47
94
  }
48
95
  function runMode(database, runId) {
49
- return database.prepare('SELECT mode FROM dsh_completion_runs WHERE run_id = ?').get(runId)?.mode ?? 'shadow';
96
+ return (database
97
+ .prepare("SELECT mode FROM dsh_completion_runs WHERE run_id = ?")
98
+ .get(runId)?.mode ?? "shadow");
50
99
  }
51
100
  function criteriaForRun(database, runId) {
52
101
  const coding = readCodingWorkflow(database, runId);
53
102
  if (coding)
54
- return { repositoryRoot: coding.repositoryRoot, enno: database.prepare("SELECT 1 FROM dsh_execution_selections WHERE run_id=? AND json_extract(state_json,'$.mode')='enno'").get(runId) !== undefined,
55
- criteria: (coding.definition?.criteria ?? []).map(item => ({ id: `coding:${coding.workflowId}:${item.criterionId}`, description: `${item.outcome} / ${item.procedure} / ${item.expected}`, revision: item.revision, approval: 'explicit' })) };
56
- const row = database.prepare(`SELECT contract.repository_root AS root, contract.workspace,
103
+ return {
104
+ repositoryRoot: coding.repositoryRoot,
105
+ enno: database
106
+ .prepare("SELECT 1 FROM dsh_execution_selections WHERE run_id=? AND json_extract(state_json,'$.mode')='enno'")
107
+ .get(runId) !== undefined,
108
+ criteria: (coding.definition?.criteria ?? []).map((item) => ({
109
+ id: `coding:${coding.workflowId}:${item.criterionId}`,
110
+ description: `${item.outcome} / ${item.procedure} / ${item.expected}`,
111
+ revision: item.revision,
112
+ approval: "explicit",
113
+ })),
114
+ };
115
+ const row = database
116
+ .prepare(`SELECT contract.repository_root AS root, contract.workspace,
57
117
  contract.orchestration_session_id AS orchestrationId FROM ledger_runs AS ledger
58
118
  LEFT JOIN enno_contracts AS contract ON contract.run_id = ledger.run_id WHERE ledger.run_id = ?`)
59
119
  .get(runId);
60
120
  if (!row)
61
- throw new KiokukoError('NOT_FOUND', 'Task completion run does not exist');
121
+ throw new KiokukoError("NOT_FOUND", "Task completion run does not exist");
62
122
  if (row.workspace && row.orchestrationId && row.root) {
63
- const snapshot = completionVerifiers().criteria(database, { runId, workspace: row.workspace, orchestrationId: row.orchestrationId });
64
- return { repositoryRoot: row.root, enno: true, criteria: snapshot.criteria.map(item => ({
65
- id: item.id, description: item.description, revision: snapshot.revision, approval: 'approved',
66
- })) };
123
+ const snapshot = completionVerifiers().criteria(database, {
124
+ runId,
125
+ workspace: row.workspace,
126
+ orchestrationId: row.orchestrationId,
127
+ });
128
+ return {
129
+ repositoryRoot: row.root,
130
+ enno: true,
131
+ criteria: snapshot.criteria.map((item) => ({
132
+ id: item.id,
133
+ description: item.description,
134
+ revision: snapshot.revision,
135
+ approval: "approved",
136
+ })),
137
+ };
67
138
  }
68
139
  const frame = readExecutionFrame(database, runId);
69
- return { repositoryRoot: frame?.workspace ?? '', enno: false,
70
- criteria: (frame?.conditions ?? []).filter(item => item.field === 'completion' && item.approval !== 'proposed')
71
- .map((item, index) => ({ id: `condition-${canonicalContentHash({ runId, revision: frame.revision, index,
72
- text: item.text, source: item.source }).slice(0, 24)}`, description: item.text, revision: frame.revision,
73
- approval: item.approval })) };
140
+ return {
141
+ repositoryRoot: frame?.workspace ?? "",
142
+ enno: false,
143
+ criteria: (frame?.conditions ?? [])
144
+ .filter((item) => item.field === "completion" && item.approval !== "proposed")
145
+ .map((item, index) => ({
146
+ id: `condition-${canonicalContentHash({
147
+ runId,
148
+ revision: frame.revision,
149
+ index,
150
+ text: item.text,
151
+ source: item.source,
152
+ }).slice(0, 24)}`,
153
+ description: item.text,
154
+ revision: frame.revision,
155
+ approval: item.approval,
156
+ })),
157
+ };
74
158
  }
75
159
  /** Advice reads the same immutable criterion contract as binding; it cannot supply execution proof. */
76
160
  export function taskCompletionBindingGuide(database, runId) {
77
161
  const source = criteriaForRun(database, runId), coding = readCodingWorkflow(database, runId);
78
- return { ...structuredClone(COMPLETION_BINDING_GUIDE), kind: source.enno ? 'enno_verifier' : 'native_command', criteria: source.criteria.map(criterion => {
79
- const definition = coding?.definition?.criteria.find(item => criterion.id === `coding:${coding.workflowId}:${item.criterionId}`);
80
- return { criterionId: criterion.id, requiredAssertion: definition ? assertionForCriterion(definition.evidence) : null };
81
- }) };
162
+ return {
163
+ ...structuredClone(COMPLETION_BINDING_GUIDE),
164
+ kind: source.enno ? "enno_verifier" : "native_command",
165
+ criteria: source.criteria.map((criterion) => {
166
+ const definition = coding?.definition?.criteria.find((item) => criterion.id === `coding:${coding.workflowId}:${item.criterionId}`);
167
+ return {
168
+ criterionId: criterion.id,
169
+ requiredAssertion: definition
170
+ ? assertionForCriterion(definition.evidence)
171
+ : null,
172
+ };
173
+ }),
174
+ };
82
175
  }
83
176
  function binding(database, runId, criterionId) {
84
- return database.prepare('SELECT * FROM dsh_completion_bindings WHERE run_id = ? AND criterion_id = ?')
177
+ return database
178
+ .prepare("SELECT * FROM dsh_completion_bindings WHERE run_id = ? AND criterion_id = ?")
85
179
  .get(runId, criterionId);
86
180
  }
87
181
  function validateMethod(database, runId, method, repositoryRoot, enno) {
88
182
  const coding = readCodingWorkflow(database, runId);
89
- if (coding && method.assertion === 'exit_zero')
90
- throw new KiokukoError('VALIDATION_ERROR', 'Coding criteria require selected tests or observable output/artifact assertions, not exit 0 alone');
91
- if (enno !== (method.kind === 'enno_verifier'))
92
- throw new KiokukoError('VALIDATION_ERROR', 'Completion method does not match execution mode', { field: 'method.kind', expected: enno ? 'enno_verifier' : 'native_command' });
93
- if (method.kind === 'native_command') {
183
+ if (coding && method.assertion === "exit_zero")
184
+ throw new KiokukoError("VALIDATION_ERROR", "Coding criteria require selected tests or observable output/artifact assertions, not exit 0 alone");
185
+ if (enno !== (method.kind === "enno_verifier"))
186
+ throw new KiokukoError("VALIDATION_ERROR", "Completion method does not match execution mode", {
187
+ field: "method.kind",
188
+ expected: enno ? "enno_verifier" : "native_command",
189
+ });
190
+ if (method.kind === "native_command") {
94
191
  const cwd = realpathSync(path.resolve(repositoryRoot, method.cwd));
95
192
  const relative = path.relative(realpathSync(repositoryRoot), cwd);
96
- if (relative.startsWith('..') || path.isAbsolute(relative))
97
- throw new KiokukoError('SECURITY_REJECTION', 'Completion cwd must be inside the repository');
193
+ if (relative.startsWith("..") || path.isAbsolute(relative))
194
+ throw new KiokukoError("SECURITY_REJECTION", "Completion cwd must be inside the repository");
98
195
  applicationSourceDigest(repositoryRoot, method.sourcePaths);
99
196
  // The pre-bound command selects the operation; complete process output,
100
197
  // rather than a command-name allowlist, establishes test coverage.
101
198
  }
102
199
  else {
103
- const row = database.prepare('SELECT contract_json FROM enno_contracts WHERE run_id = ?').get(runId);
104
- const contract = row ? JSON.parse(row.contract_json) : undefined;
105
- const verifiers = method.workUnitId ? contract?.workPlan?.units.find(unit => unit.id === method.workUnitId)?.focusedVerifiers : contract?.finalVerifiers;
106
- const verifier = verifiers?.find(item => item.id === method.verifierId);
200
+ const row = database
201
+ .prepare("SELECT contract_json FROM enno_contracts WHERE run_id = ?")
202
+ .get(runId);
203
+ const contract = row
204
+ ? JSON.parse(row.contract_json)
205
+ : undefined;
206
+ const verifiers = method.workUnitId
207
+ ? contract?.workPlan?.units.find((unit) => unit.id === method.workUnitId)
208
+ ?.focusedVerifiers
209
+ : contract?.finalVerifiers;
210
+ const verifier = verifiers?.find((item) => item.id === method.verifierId);
107
211
  if (!verifier)
108
- throw new KiokukoError('VALIDATION_ERROR', 'Completion verifier is absent from the approved Enno contract', { field: 'method.verifierId', expected: 'approved_contract_verifier' });
109
- if (!coding && method.assertion === 'selected_tests_pass' && !selectedNodeTestCommand([verifier.executable, ...verifier.args].join(' '))) {
110
- throw new KiokukoError('VALIDATION_ERROR', 'Selected-test proof requires an explicit Node TAP verifier');
212
+ throw new KiokukoError("VALIDATION_ERROR", "Completion verifier is absent from the approved Enno contract", { field: "method.verifierId", expected: "approved_contract_verifier" });
213
+ if (!coding &&
214
+ method.assertion === "selected_tests_pass" &&
215
+ !selectedNodeTestCommand([verifier.executable, ...verifier.args].join(" "))) {
216
+ throw new KiokukoError("VALIDATION_ERROR", "Selected-test proof requires an explicit Node TAP verifier");
111
217
  }
112
218
  }
113
219
  }
114
220
  export function bindTaskCriterion(database, input) {
115
221
  const method = CompletionMethodSchema.parse(input.method);
116
- const requestDigest = canonicalContentHash({ criterionId: input.criterionId, method });
117
- const previous = database.prepare('SELECT request_digest, response_json FROM dsh_completion_bind_ops WHERE run_id = ? AND call_id = ?')
222
+ const requestDigest = canonicalContentHash({
223
+ criterionId: input.criterionId,
224
+ method,
225
+ });
226
+ const previous = database
227
+ .prepare("SELECT request_digest, response_json FROM dsh_completion_bind_ops WHERE run_id = ? AND call_id = ?")
118
228
  .get(input.runId, input.callId);
119
229
  if (previous) {
120
230
  if (previous.request_digest !== requestDigest)
121
- throw new KiokukoError('CONFLICT', 'Task completion call ID was reused with changed input');
231
+ throw new KiokukoError("CONFLICT", "Task completion call ID was reused with changed input");
122
232
  return JSON.parse(previous.response_json);
123
233
  }
124
234
  const source = criteriaForRun(database, input.runId);
125
- const criterion = source.criteria.find(item => item.id === input.criterionId);
235
+ const criterion = source.criteria.find((item) => item.id === input.criterionId);
126
236
  if (!criterion)
127
- throw new KiokukoError('CONFLICT', 'Completion criterion is absent or its revision changed');
237
+ throw new KiokukoError("CONFLICT", "Completion criterion is absent or its revision changed");
128
238
  const codingWorkflow = readCodingWorkflow(database, input.runId);
129
- const codingCriterion = codingWorkflow?.definition?.criteria.find(item => input.criterionId === `coding:${codingWorkflow.workflowId}:${item.criterionId}`);
130
- const requiredAssertion = assertionForCriterion(codingCriterion?.evidence ?? 'selected_tests');
239
+ const codingCriterion = codingWorkflow?.definition?.criteria.find((item) => input.criterionId ===
240
+ `coding:${codingWorkflow.workflowId}:${item.criterionId}`);
241
+ const requiredAssertion = assertionForCriterion(codingCriterion?.evidence ?? "selected_tests");
131
242
  if (codingCriterion && method.assertion !== requiredAssertion)
132
- throw new KiokukoError('VALIDATION_ERROR', 'Bound assertion does not match this criterion’s required evidence', { field: 'method.assertion', expected: requiredAssertion });
133
- if (method.kind === 'native_command' && method.artifactPath && !method.sourcePaths.includes(method.artifactPath))
134
- throw new KiokukoError('VALIDATION_ERROR', 'Include the asserted artifact in sourcePaths so changes invalidate its evidence', { field: 'method.sourcePaths', expected: 'include_asserted_artifact' });
243
+ throw new KiokukoError("VALIDATION_ERROR", "Bound assertion does not match this criterion’s required evidence", { field: "method.assertion", expected: requiredAssertion });
244
+ if (method.kind === "native_command" &&
245
+ method.artifactPath &&
246
+ !method.sourcePaths.includes(method.artifactPath))
247
+ throw new KiokukoError("VALIDATION_ERROR", "Include the asserted artifact in sourcePaths so changes invalidate its evidence", { field: "method.sourcePaths", expected: "include_asserted_artifact" });
135
248
  validateMethod(database, input.runId, method, source.repositoryRoot, source.enno);
136
- const approved = input.approved || criterion.description.trim() === (method.kind === 'native_command'
137
- ? method.command : method.verifierId);
249
+ const approved = input.approved ||
250
+ criterion.description.trim() ===
251
+ (method.kind === "native_command" ? method.command : method.verifierId);
138
252
  // One approved command may substantiate several criteria in the same
139
253
  // revision, but never a criterion from a later contract revision.
140
254
  const coding = readCodingWorkflow(database, input.runId);
141
- const methodDigest = canonicalContentHash({ revision: criterion.revision, method, ...(coding ? { scope: coding.scopeVersion } : {}) });
255
+ const methodDigest = canonicalContentHash({
256
+ revision: criterion.revision,
257
+ method,
258
+ ...(coding ? { scope: coding.scopeVersion } : {}),
259
+ });
142
260
  const response = { criterionId: criterion.id, approved, methodDigest };
143
261
  withImmediateTransaction(database, () => {
144
262
  const prior = binding(database, input.runId, criterion.id);
145
- if (!prior || prior.criterion_revision !== criterion.revision || prior.method_digest !== methodDigest
146
- || prior.approved !== (approved ? 1 : 0)) {
147
- database.prepare(`INSERT INTO dsh_completion_bindings(run_id,criterion_id,criterion_revision,method_json,method_digest,approved,updated_at)
263
+ if (!prior ||
264
+ prior.criterion_revision !== criterion.revision ||
265
+ prior.method_digest !== methodDigest ||
266
+ prior.approved !== (approved ? 1 : 0)) {
267
+ database
268
+ .prepare(`INSERT INTO dsh_completion_bindings(run_id,criterion_id,criterion_revision,method_json,method_digest,approved,updated_at)
148
269
  VALUES(?,?,?,?,?,?,?) ON CONFLICT(run_id,criterion_id) DO UPDATE SET
149
270
  criterion_revision=excluded.criterion_revision,method_json=excluded.method_json,method_digest=excluded.method_digest,
150
271
  approved=excluded.approved,updated_at=excluded.updated_at`)
151
272
  .run(input.runId, criterion.id, criterion.revision, JSON.stringify(method), methodDigest, approved ? 1 : 0, new Date().toISOString());
152
273
  }
153
- if (approved && method.kind === 'native_command' && method.protocol)
154
- enrollProtocol(database, { runId: input.runId, sessionId: coding.sessionId, repositoryRoot: source.repositoryRoot }, { criterionId: input.criterionId, methodDigest, command: method.command, cwd: method.cwd, sourcePaths: method.sourcePaths, recipe: method.protocol });
274
+ if (approved && method.kind === "native_command" && method.protocol)
275
+ enrollProtocol(database, {
276
+ runId: input.runId,
277
+ sessionId: coding.sessionId,
278
+ repositoryRoot: source.repositoryRoot,
279
+ }, {
280
+ criterionId: input.criterionId,
281
+ methodDigest,
282
+ command: method.command,
283
+ cwd: method.cwd,
284
+ sourcePaths: method.sourcePaths,
285
+ recipe: method.protocol,
286
+ });
155
287
  else
156
288
  invalidateProtocolBinding(database, input.runId, input.criterionId, methodDigest);
157
- database.prepare('INSERT INTO dsh_completion_bind_ops(run_id,call_id,request_digest,response_json) VALUES(?,?,?,?)')
289
+ database
290
+ .prepare("INSERT INTO dsh_completion_bind_ops(run_id,call_id,request_digest,response_json) VALUES(?,?,?,?)")
158
291
  .run(input.runId, input.callId, requestDigest, JSON.stringify(response));
159
292
  });
160
293
  return response;
161
294
  }
162
295
  export function readTaskCriterionBindingReplay(database, runId, callId, criterionId, method) {
163
296
  const parsed = CompletionMethodSchema.parse(method);
164
- const row = database.prepare('SELECT request_digest, response_json FROM dsh_completion_bind_ops WHERE run_id = ? AND call_id = ?')
297
+ const row = database
298
+ .prepare("SELECT request_digest, response_json FROM dsh_completion_bind_ops WHERE run_id = ? AND call_id = ?")
165
299
  .get(runId, callId);
166
300
  if (!row)
167
301
  return undefined;
168
302
  if (row.request_digest !== canonicalContentHash({ criterionId, method: parsed })) {
169
- throw new KiokukoError('CONFLICT', 'Task completion call ID was reused with changed input');
303
+ throw new KiokukoError("CONFLICT", "Task completion call ID was reused with changed input");
170
304
  }
171
305
  return JSON.parse(row.response_json);
172
306
  }
173
307
  export function beginTaskCompletionExecution(database, input) {
174
308
  const source = criteriaForRun(database, input.runId);
175
- if (source.enno || realpathSync(input.repositoryRoot) !== realpathSync(source.repositoryRoot))
309
+ if (source.enno ||
310
+ realpathSync(input.repositoryRoot) !== realpathSync(source.repositoryRoot))
176
311
  return false;
177
- const rows = database.prepare('SELECT * FROM dsh_completion_bindings WHERE run_id = ? AND approved = 1')
312
+ const rows = database
313
+ .prepare("SELECT * FROM dsh_completion_bindings WHERE run_id = ? AND approved = 1")
178
314
  .all(input.runId);
179
- const matching = rows.filter(row => {
315
+ const matching = rows.filter((row) => {
180
316
  const method = CompletionMethodSchema.parse(JSON.parse(row.method_json));
181
- return method.kind === 'native_command' && executionCommand(method.command) === executionCommand(input.command)
182
- && realpathSync(path.resolve(source.repositoryRoot, method.cwd)) === realpathSync(input.cwd ?? input.repositoryRoot)
183
- && source.criteria.some(criterion => criterion.id === row.criterion_id && criterion.revision === row.criterion_revision);
317
+ return (method.kind === "native_command" &&
318
+ executionCommand(method.command) === executionCommand(input.command) &&
319
+ realpathSync(path.resolve(source.repositoryRoot, method.cwd)) ===
320
+ realpathSync(input.cwd ?? input.repositoryRoot) &&
321
+ source.criteria.some((criterion) => criterion.id === row.criterion_id &&
322
+ criterion.revision === row.criterion_revision));
184
323
  });
185
324
  const coding = readCodingWorkflow(database, input.runId);
186
- const assessment = coding ? assessTaskCompletion(database, input.runId) : undefined;
187
- const goalReady = coding?.definition?.items.every(item => item.criterionIds.every(id => assessment.criteria.some(criterion => criterion.criterionId === `coding:${coding.workflowId}:${id}` && criterion.state === 'satisfied'))) ?? false;
188
- const isGoal = (row) => coding?.definition?.goal.criterionIds.some(id => row.criterion_id === `coding:${coding.workflowId}:${id}`);
325
+ const assessment = coding
326
+ ? assessTaskCompletion(database, input.runId)
327
+ : undefined;
328
+ const goalReady = coding?.definition?.items.every((item) => item.criterionIds.every((id) => assessment.criteria.some((criterion) => criterion.criterionId === `coding:${coding.workflowId}:${id}` &&
329
+ criterion.state === "satisfied"))) ?? false;
330
+ const isGoal = (row) => coding?.definition?.goal.criterionIds.some((id) => row.criterion_id === `coding:${coding.workflowId}:${id}`);
189
331
  // A protocol recipe changes the item method identity. Observe final Goal
190
332
  // bindings separately, only after item evidence exists on the current source.
191
333
  const current = coding?.protocol
192
334
  ? goalReady
193
- ? matching.find(row => isGoal(row) && assessment.criteria.find(item => item.criterionId === row.criterion_id)?.state !== 'satisfied') ?? matching.find(isGoal) ?? matching[0]
194
- : matching.find(row => row.criterion_id === coding.protocol.criterionId) ?? matching[0]
335
+ ? (matching.find((row) => isGoal(row) &&
336
+ assessment.criteria.find((item) => item.criterionId === row.criterion_id)?.state !== "satisfied") ??
337
+ matching.find(isGoal) ??
338
+ matching[0])
339
+ : (matching.find((row) => row.criterion_id === coding.protocol.criterionId) ?? matching[0])
195
340
  : matching[0];
196
341
  if (!current)
197
342
  return false;
198
343
  const method = CompletionMethodSchema.parse(JSON.parse(current.method_json));
199
- if (method.kind !== 'native_command')
344
+ if (method.kind !== "native_command")
200
345
  return false;
201
346
  const digest = completionSourceDigest(database, input.runId, source.repositoryRoot, method.sourcePaths);
202
- const codingInputs = coding?.definition ? coding.definition.items.flatMap(item => item.criterionIds.map(id => ({ id,
203
- evidenceRef: assessment.criteria.find(criterion => criterion.criterionId === `coding:${coding.workflowId}:${id}`)?.evidenceRef ?? null }))) : null;
204
- const existing = database.prepare('SELECT method_digest, source_digest, outcome FROM dsh_completion_executions WHERE run_id = ? AND call_id = ?')
347
+ const codingInputs = coding?.definition
348
+ ? coding.definition.items.flatMap((item) => item.criterionIds.map((id) => ({
349
+ id,
350
+ evidenceRef: assessment.criteria.find((criterion) => criterion.criterionId === `coding:${coding.workflowId}:${id}`)?.evidenceRef ?? null,
351
+ })))
352
+ : null;
353
+ const existing = database
354
+ .prepare("SELECT method_digest, source_digest, outcome FROM dsh_completion_executions WHERE run_id = ? AND call_id = ?")
205
355
  .get(input.runId, input.callId);
206
356
  if (existing) {
207
- if (existing.method_digest !== current.method_digest || existing.source_digest !== digest) {
208
- throw new KiokukoError('CONFLICT', 'Task completion execution ID was reused with changed bound input');
357
+ if (existing.method_digest !== current.method_digest ||
358
+ existing.source_digest !== digest) {
359
+ throw new KiokukoError("CONFLICT", "Task completion execution ID was reused with changed bound input");
209
360
  }
210
- return existing.outcome === 'started';
361
+ return existing.outcome === "started";
211
362
  }
212
- database.prepare(`INSERT OR IGNORE INTO dsh_completion_executions
363
+ database
364
+ .prepare(`INSERT OR IGNORE INTO dsh_completion_executions
213
365
  (run_id,call_id,method_digest,source_digest,outcome,updated_at,coding_scope,coding_goal_ready,coding_inputs_json) VALUES(?,?,?,?,'started',?,?,?,?)`)
214
366
  .run(input.runId, input.callId, current.method_digest, digest, new Date().toISOString(), coding?.scopeVersion ?? null, goalReady ? 1 : 0, codingInputs ? JSON.stringify(codingInputs) : null);
215
367
  return true;
216
368
  }
217
369
  export function finishTaskCompletionExecution(database, input) {
218
- const row = database.prepare('SELECT * FROM dsh_completion_executions WHERE run_id = ? AND call_id = ?')
370
+ const row = database
371
+ .prepare("SELECT * FROM dsh_completion_executions WHERE run_id = ? AND call_id = ?")
219
372
  .get(input.runId, input.callId);
220
- if (!row || row.outcome !== 'started')
373
+ if (!row || row.outcome !== "started")
221
374
  return;
222
375
  const source = criteriaForRun(database, input.runId);
223
- const bindingRow = database.prepare('SELECT * FROM dsh_completion_bindings WHERE run_id = ? AND method_digest = ?')
376
+ const bindingRow = database
377
+ .prepare("SELECT * FROM dsh_completion_bindings WHERE run_id = ? AND method_digest = ?")
224
378
  .get(input.runId, row.method_digest);
225
379
  if (!bindingRow)
226
380
  return;
227
381
  const method = CompletionMethodSchema.parse(JSON.parse(bindingRow.method_json));
228
- if (method.kind !== 'native_command')
382
+ if (method.kind !== "native_command")
229
383
  return;
230
384
  let sourceDigest;
231
385
  try {
232
386
  sourceDigest = completionSourceDigest(database, input.runId, source.repositoryRoot, method.sourcePaths);
233
387
  }
234
- catch { /* stale or unavailable */ }
388
+ catch {
389
+ /* stale or unavailable */
390
+ }
235
391
  const response = input.result;
236
392
  const exit = response?.value?.exitCode ?? response?.value?.exit_code;
237
- const content = typeof response?.content === 'string' ? response.content : Array.isArray(response?.content)
238
- ? response.content.map(block => typeof block?.text === 'string' ? block.text : '').join('\n') : '';
393
+ const content = typeof response?.content === "string"
394
+ ? response.content
395
+ : Array.isArray(response?.content)
396
+ ? response.content
397
+ .map((block) => (typeof block?.text === "string" ? block.text : ""))
398
+ .join("\n")
399
+ : "";
239
400
  const coding = readCodingWorkflow(database, input.runId);
240
- const tap = method.assertion === 'selected_tests_pass' && (!coding || hasExplicitTestCases(content))
241
- ? parseTestSummary(content, isHostExecutionResult(input.result)) : undefined;
242
- const observable = method.assertion === 'output_matches' ? content.replace(/\r\n/gu, '\n').trimEnd() === method.expectedOutput.replace(/\r\n/gu, '\n').trimEnd() && isHostExecutionResult(input.result)
243
- : method.assertion === 'artifact_matches' ? artifactMatches(source.repositoryRoot, method) : true;
244
- const outcome = sourceDigest !== row.source_digest || coding && (row.coding_scope !== coding.scopeVersion || ['cancelled', 'superseded'].includes(coding.state)) ? 'stale'
245
- : response?.isError || response?.value?.timedOut || response?.value?.aborted || response?.value?.signal || response?.value?.kind === 'background'
246
- || Number.isSafeInteger(exit) && exit !== 0 ? 'failed'
247
- : coding && !isHostExecutionResult(input.result) || !Number.isSafeInteger(exit) || method.assertion === 'exit_zero' && hasShellPipeline(method.command) || method.assertion === 'selected_tests_pass' && !tap ? 'unknown'
248
- : tap && (tap.fail > 0 || tap.cancelled > 0 || tap.skipped > 0 || tap.todo > 0) || !observable ? 'failed' : 'passed';
249
- database.prepare(`UPDATE dsh_completion_executions SET outcome=?,exit_code=?,tap_summary_json=?,result_digest=?,updated_at=?
401
+ const tap = method.assertion === "selected_tests_pass" &&
402
+ (!coding || hasExplicitTestCases(content))
403
+ ? parseTestSummary(content, isHostExecutionResult(input.result))
404
+ : undefined;
405
+ const observable = method.assertion === "output_matches"
406
+ ? content.replace(/\r\n/gu, "\n").trimEnd() ===
407
+ method.expectedOutput.replace(/\r\n/gu, "\n").trimEnd() &&
408
+ isHostExecutionResult(input.result)
409
+ : method.assertion === "artifact_matches"
410
+ ? artifactMatches(source.repositoryRoot, method)
411
+ : true;
412
+ const outcome = sourceDigest !== row.source_digest ||
413
+ (coding &&
414
+ (row.coding_scope !== coding.scopeVersion ||
415
+ ["cancelled", "superseded"].includes(coding.state)))
416
+ ? "stale"
417
+ : response?.isError ||
418
+ response?.value?.timedOut ||
419
+ response?.value?.aborted ||
420
+ response?.value?.signal ||
421
+ response?.value?.kind === "background" ||
422
+ (Number.isSafeInteger(exit) && exit !== 0)
423
+ ? "failed"
424
+ : ((coding || method.assertion === "output_matches") &&
425
+ !isHostExecutionResult(input.result)) ||
426
+ !Number.isSafeInteger(exit) ||
427
+ (method.assertion === "exit_zero" &&
428
+ hasShellPipeline(method.command)) ||
429
+ (method.assertion === "selected_tests_pass" && !tap)
430
+ ? "unknown"
431
+ : (tap &&
432
+ (tap.fail > 0 ||
433
+ tap.cancelled > 0 ||
434
+ tap.skipped > 0 ||
435
+ tap.todo > 0)) ||
436
+ !observable
437
+ ? "failed"
438
+ : "passed";
439
+ database
440
+ .prepare(`UPDATE dsh_completion_executions SET outcome=?,exit_code=?,tap_summary_json=?,result_digest=?,updated_at=?
250
441
  WHERE run_id=? AND call_id=? AND outcome='started'`)
251
- .run(outcome, Number.isSafeInteger(exit) ? exit : null, tap ? JSON.stringify(tap) : null, canonicalContentHash({ isError: response?.isError === true, value: response?.value, content: response?.content }), new Date().toISOString(), input.runId, input.callId);
252
- if (coding?.definition && (outcome === 'failed' || outcome === 'unknown')) {
253
- const fingerprint = canonicalContentHash({ method: row.method_digest, source: row.source_digest, outcome, exit: exit ?? null, tap: tap ?? null });
254
- const affected = database.prepare('SELECT criterion_id FROM dsh_completion_bindings WHERE run_id=? AND method_digest=?')
442
+ .run(outcome, Number.isSafeInteger(exit) ? exit : null, tap ? JSON.stringify(tap) : null, canonicalContentHash({
443
+ isError: response?.isError === true,
444
+ value: response?.value,
445
+ content: response?.content,
446
+ }), new Date().toISOString(), input.runId, input.callId);
447
+ if (coding?.definition && (outcome === "failed" || outcome === "unknown")) {
448
+ const fingerprint = canonicalContentHash({
449
+ method: row.method_digest,
450
+ source: row.source_digest,
451
+ outcome,
452
+ exit: exit ?? null,
453
+ tap: tap ?? null,
454
+ });
455
+ const affected = database
456
+ .prepare("SELECT criterion_id FROM dsh_completion_bindings WHERE run_id=? AND method_digest=?")
255
457
  .all(input.runId, row.method_digest);
256
458
  for (const affectedCriterion of affected) {
257
459
  const id = affectedCriterion.criterion_id.slice(`coding:${coding.workflowId}:`.length);
258
- if (coding.definition.goal.criterionIds.includes(id) && row.coding_goal_ready !== 1)
460
+ if (coding.definition.goal.criterionIds.includes(id) &&
461
+ row.coding_goal_ready !== 1)
259
462
  continue;
260
463
  recordCodingAttempt(database, coding, `failure:${input.callId}:${id}`, id, fingerprint, Date.now(), row.source_digest);
261
464
  }
@@ -264,126 +467,283 @@ export function finishTaskCompletionExecution(database, input) {
264
467
  export function assessTaskCompletion(database, runId) {
265
468
  const source = criteriaForRun(database, runId);
266
469
  const coding = readCodingWorkflow(database, runId);
267
- const assessments = source.criteria.map(criterion => {
470
+ const assessments = source.criteria.map((criterion) => {
268
471
  const row = binding(database, runId, criterion.id);
269
472
  if (!row || row.approved !== 1)
270
- return { criterionId: criterion.id, description: criterion.description,
271
- state: 'unknown', reason: row ? 'binding_not_approved' : 'no_binding' };
473
+ return {
474
+ criterionId: criterion.id,
475
+ description: criterion.description,
476
+ state: "unknown",
477
+ reason: row ? "binding_not_approved" : "no_binding",
478
+ };
272
479
  if (row.criterion_revision !== criterion.revision)
273
- return { criterionId: criterion.id, description: criterion.description,
274
- state: 'stale', reason: 'criterion_changed' };
480
+ return {
481
+ criterionId: criterion.id,
482
+ description: criterion.description,
483
+ state: "stale",
484
+ reason: "criterion_changed",
485
+ };
275
486
  const method = CompletionMethodSchema.parse(JSON.parse(row.method_json));
276
- if (method.kind === 'enno_verifier') {
277
- const contractRow = database.prepare('SELECT workspace, orchestration_session_id AS orchestrationId FROM enno_contracts WHERE run_id = ?')
487
+ if (method.kind === "enno_verifier") {
488
+ const contractRow = database
489
+ .prepare("SELECT workspace, orchestration_session_id AS orchestrationId FROM enno_contracts WHERE run_id = ?")
278
490
  .get(runId);
279
491
  const fresh = completionVerifiers().results(database, { runId, ...contractRow }, source.repositoryRoot, method.workUnitId);
280
- const result = fresh?.find(item => item.verifierId === method.verifierId);
492
+ const result = fresh?.find((item) => item.verifierId === method.verifierId);
281
493
  if (!result)
282
- return { criterionId: criterion.id, description: criterion.description, state: 'stale', reason: 'verifier_evidence_missing_or_stale' };
283
- const complete = method.assertion === 'exit_zero' || (!coding || result.explicitCases === true) && result.tapSummary !== undefined && result.tapSummary.tests > 0
284
- && result.tapSummary.pass === result.tapSummary.tests && result.tapSummary.skipped === 0 && result.tapSummary.todo === 0;
285
- const skipped = method.assertion === 'selected_tests_pass' && result.tapSummary !== undefined
286
- && (result.tapSummary.skipped > 0 || result.tapSummary.todo > 0 || result.tapSummary.fail > 0);
287
- return { criterionId: criterion.id, description: criterion.description,
288
- state: result.status !== 'passed' || skipped ? 'unmet' : complete ? 'satisfied' : 'unknown',
289
- reason: result.status !== 'passed' ? 'verifier_failed' : skipped ? 'selected_tests_unmet'
290
- : complete ? 'fresh_verifier' : 'selected_test_coverage_unknown',
291
- evidenceRef: result.receiptId ?? `${runId}:${method.verifierId}` };
494
+ return {
495
+ criterionId: criterion.id,
496
+ description: criterion.description,
497
+ state: "stale",
498
+ reason: "verifier_evidence_missing_or_stale",
499
+ };
500
+ const complete = method.assertion === "exit_zero" ||
501
+ ((!coding || result.explicitCases === true) &&
502
+ result.tapSummary !== undefined &&
503
+ result.tapSummary.tests > 0 &&
504
+ result.tapSummary.pass === result.tapSummary.tests &&
505
+ result.tapSummary.skipped === 0 &&
506
+ result.tapSummary.todo === 0);
507
+ const skipped = method.assertion === "selected_tests_pass" &&
508
+ result.tapSummary !== undefined &&
509
+ (result.tapSummary.skipped > 0 ||
510
+ result.tapSummary.todo > 0 ||
511
+ result.tapSummary.fail > 0);
512
+ return {
513
+ criterionId: criterion.id,
514
+ description: criterion.description,
515
+ state: result.status !== "passed" || skipped
516
+ ? "unmet"
517
+ : complete
518
+ ? "satisfied"
519
+ : "unknown",
520
+ reason: result.status !== "passed"
521
+ ? "verifier_failed"
522
+ : skipped
523
+ ? "selected_tests_unmet"
524
+ : complete
525
+ ? "fresh_verifier"
526
+ : "selected_test_coverage_unknown",
527
+ evidenceRef: result.receiptId ?? `${runId}:${method.verifierId}`,
528
+ };
292
529
  }
293
- const executions = database.prepare(`SELECT * FROM dsh_completion_executions WHERE run_id = ? AND method_digest = ?
294
- ORDER BY updated_at DESC, rowid DESC LIMIT 1`).all(runId, row.method_digest);
530
+ const executions = database
531
+ .prepare(`SELECT * FROM dsh_completion_executions WHERE run_id = ? AND method_digest = ?
532
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`)
533
+ .all(runId, row.method_digest);
295
534
  const execution = executions[0];
296
535
  if (!execution)
297
- return { criterionId: criterion.id, description: criterion.description, state: 'unknown', reason: 'command_not_observed' };
536
+ return {
537
+ criterionId: criterion.id,
538
+ description: criterion.description,
539
+ state: "unknown",
540
+ reason: "command_not_observed",
541
+ };
298
542
  let currentDigest;
299
543
  try {
300
544
  currentDigest = completionSourceDigest(database, runId, source.repositoryRoot, method.sourcePaths);
301
545
  }
302
- catch { /* unknown */ }
303
- if (execution.source_digest !== currentDigest || execution.outcome === 'stale' || coding && execution.coding_scope !== coding.scopeVersion)
304
- return { criterionId: criterion.id,
305
- description: criterion.description, state: 'stale', reason: 'source_changed', evidenceRef: execution.call_id };
306
- const tap = execution.tap_summary_json ? JSON.parse(execution.tap_summary_json) : undefined;
307
- const finalGoal = coding?.definition?.goal.criterionIds.some(id => criterion.id === `coding:${coding.workflowId}:${id}`);
546
+ catch {
547
+ /* unknown */
548
+ }
549
+ if (execution.source_digest !== currentDigest ||
550
+ execution.outcome === "stale" ||
551
+ (coding && execution.coding_scope !== coding.scopeVersion))
552
+ return {
553
+ criterionId: criterion.id,
554
+ description: criterion.description,
555
+ state: "stale",
556
+ reason: "source_changed",
557
+ evidenceRef: execution.call_id,
558
+ };
559
+ const tap = execution.tap_summary_json
560
+ ? JSON.parse(execution.tap_summary_json)
561
+ : undefined;
562
+ const finalGoal = coding?.definition?.goal.criterionIds.some((id) => criterion.id === `coding:${coding.workflowId}:${id}`);
308
563
  if (finalGoal && execution.coding_goal_ready !== 1)
309
- return { criterionId: criterion.id, description: criterion.description, state: 'unknown', reason: 'final_goal_must_run_after_item_checks', evidenceRef: execution.call_id };
310
- const complete = method.assertion === 'exit_zero' || method.assertion === 'output_matches' || method.assertion === 'artifact_matches' && artifactMatches(source.repositoryRoot, method) || tap !== undefined && tap.tests > 0 && tap.pass === tap.tests
311
- && tap.skipped === 0 && tap.todo === 0;
312
- const skipped = method.assertion === 'selected_tests_pass' && tap !== undefined
313
- && (tap.skipped > 0 || tap.todo > 0 || tap.fail > 0);
314
- return { criterionId: criterion.id, description: criterion.description,
315
- state: execution.outcome === 'failed' || skipped ? 'unmet' : execution.outcome === 'passed' && complete ? 'satisfied' : 'unknown',
316
- reason: execution.outcome === 'failed' ? 'command_failed' : skipped ? 'selected_tests_unmet'
317
- : execution.outcome === 'passed' && complete ? 'observed_command' : method.assertion === 'exit_zero' && hasShellPipeline(method.command) ? 'pipeline_stage_status_unknown_use_direct_verifier' : 'coverage_unknown',
318
- evidenceRef: execution.call_id };
564
+ return {
565
+ criterionId: criterion.id,
566
+ description: criterion.description,
567
+ state: "unknown",
568
+ reason: "final_goal_must_run_after_item_checks",
569
+ evidenceRef: execution.call_id,
570
+ };
571
+ const complete = method.assertion === "exit_zero" ||
572
+ method.assertion === "output_matches" ||
573
+ (method.assertion === "artifact_matches" &&
574
+ artifactMatches(source.repositoryRoot, method)) ||
575
+ (tap !== undefined &&
576
+ tap.tests > 0 &&
577
+ tap.pass === tap.tests &&
578
+ tap.skipped === 0 &&
579
+ tap.todo === 0);
580
+ const skipped = method.assertion === "selected_tests_pass" &&
581
+ tap !== undefined &&
582
+ (tap.skipped > 0 || tap.todo > 0 || tap.fail > 0);
583
+ return {
584
+ criterionId: criterion.id,
585
+ description: criterion.description,
586
+ state: execution.outcome === "failed" || skipped
587
+ ? "unmet"
588
+ : execution.outcome === "passed" && complete
589
+ ? "satisfied"
590
+ : "unknown",
591
+ reason: execution.outcome === "failed"
592
+ ? "command_failed"
593
+ : skipped
594
+ ? "selected_tests_unmet"
595
+ : execution.outcome === "passed" && complete
596
+ ? "observed_command"
597
+ : method.assertion === "exit_zero" &&
598
+ hasShellPipeline(method.command)
599
+ ? "pipeline_stage_status_unknown_use_direct_verifier"
600
+ : "coverage_unknown",
601
+ evidenceRef: execution.call_id,
602
+ };
319
603
  });
320
604
  const managedOperationsReady = !unresolvedManaged(database, runId);
321
- const workflowReady = !coding || Boolean(protocolCompletionReady(database, runId) && coding.definition && ['yes', 'no'].includes(coding.choice.state) && !['cancelled', 'superseded', 'blocked'].includes(coding.state));
605
+ const workflowReady = !coding ||
606
+ Boolean(protocolCompletionReady(database, runId) &&
607
+ coding.definition &&
608
+ ["yes", "no"].includes(coding.choice.state) &&
609
+ !["cancelled", "superseded", "blocked"].includes(coding.state));
322
610
  if (coding?.definition) {
323
- const find = (id) => assessments.find(item => item.criterionId === `coding:${coding.workflowId}:${id}`);
611
+ const find = (id) => assessments.find((item) => item.criterionId === `coding:${coding.workflowId}:${id}`);
324
612
  const visited = new Set();
325
613
  const evaluate = (id) => {
326
- const item = coding.definition.items.find(item => item.itemId === id);
614
+ const item = coding.definition.items.find((item) => item.itemId === id);
327
615
  if (!visited.has(id)) {
328
616
  visited.add(id);
329
617
  if (!item.dependsOn.every(evaluate))
330
618
  for (const id of item.criterionIds) {
331
619
  const criterion = find(id);
332
- if (criterion.state === 'satisfied') {
333
- criterion.state = 'stale';
334
- criterion.reason = 'dependency_unmet';
620
+ if (criterion.state === "satisfied") {
621
+ criterion.state = "stale";
622
+ criterion.reason = "dependency_unmet";
335
623
  }
336
624
  }
337
625
  }
338
- return item.criterionIds.every(id => find(id).state === 'satisfied');
626
+ return item.criterionIds.every((id) => find(id).state === "satisfied");
339
627
  };
340
- const itemsReady = coding.definition.items.map(item => evaluate(item.itemId)).every(Boolean);
628
+ const itemsReady = coding.definition.items
629
+ .map((item) => evaluate(item.itemId))
630
+ .every(Boolean);
341
631
  for (const id of coding.definition.goal.criterionIds) {
342
632
  const criterion = find(id);
343
- if (criterion.state !== 'satisfied')
633
+ if (criterion.state !== "satisfied")
344
634
  continue;
345
- const row = database.prepare('SELECT coding_inputs_json FROM dsh_completion_executions WHERE run_id=? AND call_id=?')
635
+ const row = database
636
+ .prepare("SELECT coding_inputs_json FROM dsh_completion_executions WHERE run_id=? AND call_id=?")
346
637
  .get(runId, criterion.evidenceRef);
347
- const inputs = row?.coding_inputs_json ? JSON.parse(row.coding_inputs_json) : [];
348
- if (!itemsReady || inputs.some(input => find(input.id)?.evidenceRef !== input.evidenceRef && find(input.id)?.evidenceRef !== criterion.evidenceRef)) {
349
- criterion.state = 'stale';
350
- criterion.reason = 'item_evidence_changed';
638
+ const inputs = row?.coding_inputs_json
639
+ ? JSON.parse(row.coding_inputs_json)
640
+ : [];
641
+ if (!itemsReady ||
642
+ inputs.some((input) => find(input.id)?.evidenceRef !== input.evidenceRef &&
643
+ find(input.id)?.evidenceRef !== criterion.evidenceRef)) {
644
+ criterion.state = "stale";
645
+ criterion.reason = "item_evidence_changed";
351
646
  }
352
647
  }
353
648
  }
354
- return { mode: runMode(database, runId), managedOperationsReady, verified: managedOperationsReady && workflowReady && assessments.length > 0 && assessments.every(item => item.state === 'satisfied'),
355
- required: assessments.length, criteria: assessments, ...(coding ? { workflowReady, coding: { workflowId: coding.workflowId, state: coding.state, scopeVersion: coding.scopeVersion, ...(coding.protocol ? { protocol: { kind: coding.protocol.recipe.kind, phase: coding.protocol.schema === 2 ? coding.protocol.phase : 'rebind-required', ...(coding.protocol.schema !== 2 ? { reason: 'Legacy schema 1 evidence is retained read-only; explicitly rebind this unit' } : coding.protocol.reason ? { reason: coding.protocol.reason } : {}), generation: coding.protocol.generation, capabilities: PROTOCOL_CAPABILITIES } } : {}),
356
- goalCriterionIds: (coding.definition?.goal.criterionIds ?? []).map(id => `coding:${coding.workflowId}:${id}`), itemCriterionIds: (coding.definition?.items ?? []).flatMap(item => item.criterionIds.map(id => `coding:${coding.workflowId}:${id}`)) } } : {}) };
649
+ return {
650
+ mode: runMode(database, runId),
651
+ managedOperationsReady,
652
+ verified: managedOperationsReady &&
653
+ workflowReady &&
654
+ assessments.length > 0 &&
655
+ assessments.every((item) => item.state === "satisfied"),
656
+ required: assessments.length,
657
+ criteria: assessments,
658
+ ...(coding
659
+ ? {
660
+ workflowReady,
661
+ coding: {
662
+ workflowId: coding.workflowId,
663
+ state: coding.state,
664
+ scopeVersion: coding.scopeVersion,
665
+ ...(coding.protocol
666
+ ? {
667
+ protocol: {
668
+ kind: coding.protocol.recipe.kind,
669
+ phase: coding.protocol.schema === 2
670
+ ? coding.protocol.phase
671
+ : "rebind-required",
672
+ ...(coding.protocol.schema !== 2
673
+ ? {
674
+ reason: "Legacy schema 1 evidence is retained read-only; explicitly rebind this unit",
675
+ }
676
+ : coding.protocol.reason
677
+ ? { reason: coding.protocol.reason }
678
+ : {}),
679
+ generation: coding.protocol.generation,
680
+ capabilities: PROTOCOL_CAPABILITIES,
681
+ },
682
+ }
683
+ : {}),
684
+ goalCriterionIds: (coding.definition?.goal.criterionIds ?? []).map((id) => `coding:${coding.workflowId}:${id}`),
685
+ itemCriterionIds: (coding.definition?.items ?? []).flatMap((item) => item.criterionIds.map((id) => `coding:${coding.workflowId}:${id}`)),
686
+ },
687
+ }
688
+ : {}),
689
+ };
357
690
  }
358
691
  export function saveTaskCompletionReceipt(database, runId) {
359
692
  const assessment = assessTaskCompletion(database, runId);
360
- if (!database.prepare('SELECT run_id FROM dsh_completion_runs WHERE run_id = ?').get(runId))
693
+ if (!database
694
+ .prepare("SELECT run_id FROM dsh_completion_runs WHERE run_id = ?")
695
+ .get(runId))
361
696
  return assessment;
362
- database.prepare(`INSERT INTO dsh_completion_receipts(run_id,assessment_json,updated_at) VALUES(?,?,?)
697
+ database
698
+ .prepare(`INSERT INTO dsh_completion_receipts(run_id,assessment_json,updated_at) VALUES(?,?,?)
363
699
  ON CONFLICT(run_id) DO UPDATE SET assessment_json=excluded.assessment_json,updated_at=excluded.updated_at`)
364
700
  .run(runId, JSON.stringify(assessment), new Date().toISOString());
365
701
  return assessment;
366
702
  }
367
703
  export function taskCompletionBlocksClose(assessment) {
368
- return assessment.managedOperationsReady === false || assessment.mode === 'enforce' && (assessment.required > 0 || assessment.workflowReady !== undefined) && !assessment.verified;
704
+ return (assessment.managedOperationsReady === false ||
705
+ (assessment.mode === "enforce" &&
706
+ (assessment.required > 0 || assessment.workflowReady !== undefined) &&
707
+ !assessment.verified));
369
708
  }
370
709
  /** Include the consumer configuration and runtime alongside selected source paths. */
371
710
  export function completionSourceDigest(database, runId, root, paths) {
372
711
  const workflow = readCodingWorkflow(database, runId);
373
712
  if (!workflow)
374
- return canonicalContentHash({ commandIdentityVersion: EXECUTION_COMMAND_VERSION, source: applicationSourceDigest(root, paths) });
375
- const configuration = ['package.json', 'package-lock.json', 'pnpm-lock.yaml', 'tsconfig.json'].filter(file => existsSync(path.join(root, file)));
376
- return canonicalContentHash({ commandIdentityVersion: EXECUTION_COMMAND_VERSION, source: applicationSourceDigest(root, [...new Set([...paths, ...configuration])]), scopeVersion: workflow.scopeVersion,
377
- runtime: { node: process.version, executable: process.execPath, pathDigest: canonicalContentHash(process.env.PATH ?? ''), platform: process.platform, arch: process.arch } });
713
+ return canonicalContentHash({
714
+ commandIdentityVersion: EXECUTION_COMMAND_VERSION,
715
+ source: applicationSourceDigest(root, paths),
716
+ });
717
+ const configuration = [
718
+ "package.json",
719
+ "package-lock.json",
720
+ "pnpm-lock.yaml",
721
+ "tsconfig.json",
722
+ ].filter((file) => existsSync(path.join(root, file)));
723
+ return canonicalContentHash({
724
+ commandIdentityVersion: EXECUTION_COMMAND_VERSION,
725
+ source: applicationSourceDigest(root, [
726
+ ...new Set([...paths, ...configuration]),
727
+ ]),
728
+ scopeVersion: workflow.scopeVersion,
729
+ runtime: {
730
+ node: process.version,
731
+ executable: process.execPath,
732
+ pathDigest: canonicalContentHash(process.env.PATH ?? ""),
733
+ platform: process.platform,
734
+ arch: process.arch,
735
+ },
736
+ });
378
737
  }
379
738
  function artifactMatches(root, method) {
380
739
  if (!method.artifactPath || !method.artifactSha256)
381
740
  return false;
382
741
  try {
383
742
  const file = realpathSync(path.join(root, method.artifactPath)), relative = path.relative(realpathSync(root), file);
384
- if (relative.startsWith('..') || path.isAbsolute(relative))
743
+ if (relative.startsWith("..") || path.isAbsolute(relative))
385
744
  return false;
386
- return createHash('sha256').update(readFileSync(file)).digest('hex') === method.artifactSha256;
745
+ return (createHash("sha256").update(readFileSync(file)).digest("hex") ===
746
+ method.artifactSha256);
387
747
  }
388
748
  catch {
389
749
  return false;