@deksden-com/dd-flow-cli 0.9.0-beta.10 → 0.9.0-beta.103

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 (151) hide show
  1. package/CHANGELOG.md +491 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +89 -13
  6. package/dist/cli/hook-ingress.js +77 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/run-cli.js +1437 -384
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  11. package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
  12. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  13. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  14. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  15. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  16. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  17. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  18. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  19. package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
  20. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  21. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +502 -0
  22. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  23. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  24. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
  25. package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
  26. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  27. package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
  28. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +387 -0
  29. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  30. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
  31. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  32. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +640 -0
  33. package/dist/harness-runtime/lib/dd-zcode.mjs +978 -0
  34. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  35. package/dist/harness-runtime/lib/delegation-instructions.mjs +135 -0
  36. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  37. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  38. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  39. package/dist/harness-runtime/lib/managed-daemon.mjs +296 -0
  40. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  41. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  42. package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
  43. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  44. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  45. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  46. package/dist/harness-runtime/lib/process-json.mjs +71 -0
  47. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  48. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  49. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  50. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  51. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  52. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  53. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  54. package/dist/schemas/agent-profile.schema.json +1 -1
  55. package/dist/schemas/code-review-result.schema.json +1 -1
  56. package/dist/schemas/code-work-batch.schema.json +3 -3
  57. package/dist/schemas/code-work-result.schema.json +3 -3
  58. package/dist/schemas/plan-review-result.schema.json +2 -2
  59. package/dist/schemas/run-control-receipt.schema.json +99 -0
  60. package/dist/schemas/run-control-request.schema.json +36 -0
  61. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  62. package/dist/services/canon.js +11 -8
  63. package/dist/services/cleanup.js +57 -13
  64. package/dist/services/cli-operation-classifier.js +22 -24
  65. package/dist/services/code-checks.js +583 -99
  66. package/dist/services/codex-hook-delivery.js +28 -0
  67. package/dist/services/command-context.js +80 -0
  68. package/dist/services/config.js +11 -8
  69. package/dist/services/continuation-outcome.js +20 -0
  70. package/dist/services/controller-fanout.js +165 -0
  71. package/dist/services/dashboard.js +103 -31
  72. package/dist/services/engines.js +121 -76
  73. package/dist/services/eval-snapshots.js +952 -83
  74. package/dist/services/execution-policy.js +170 -0
  75. package/dist/services/external-work-launch.js +165 -0
  76. package/dist/services/harness-adapter.js +147 -24
  77. package/dist/services/harness-config.js +2 -0
  78. package/dist/services/hooks.js +596 -300
  79. package/dist/services/lanes.js +61 -53
  80. package/dist/services/lifecycle-command.js +103 -17
  81. package/dist/services/lifecycle-contract.js +30 -0
  82. package/dist/services/lifecycle-invocations.js +1128 -0
  83. package/dist/services/managed-daemon-binding.js +38 -0
  84. package/dist/services/managed-processes.js +283 -27
  85. package/dist/services/merge-queue.js +179 -104
  86. package/dist/services/merge-server.js +73 -28
  87. package/dist/services/migrations.js +14 -9
  88. package/dist/services/native-daemon-history.js +49 -0
  89. package/dist/services/native-session-control.js +39 -0
  90. package/dist/services/plan-runtime.js +19 -8
  91. package/dist/services/plans.js +28 -25
  92. package/dist/services/projects.js +18 -5
  93. package/dist/services/prompts.js +25 -16
  94. package/dist/services/protocols.js +64 -20
  95. package/dist/services/recovery-observation-budget.js +61 -0
  96. package/dist/services/recovery-snapshot-database.js +107 -0
  97. package/dist/services/repair-intents.js +40 -0
  98. package/dist/services/review-copy.js +136 -0
  99. package/dist/services/run-control-receipt.js +56 -0
  100. package/dist/services/run-control-worker.js +368 -0
  101. package/dist/services/run-control.js +876 -0
  102. package/dist/services/run-controller-adapter.js +231 -0
  103. package/dist/services/run-controller-capture.js +136 -0
  104. package/dist/services/run-controller-process.js +189 -0
  105. package/dist/services/run-controller-recovery.js +222 -0
  106. package/dist/services/run-controller-state.js +36 -0
  107. package/dist/services/run-controller.js +803 -0
  108. package/dist/services/run-engine-bindings.js +1 -1
  109. package/dist/services/run-fork.js +138 -0
  110. package/dist/services/run-observations.js +112 -0
  111. package/dist/services/run-recovery-runtime.js +69 -0
  112. package/dist/services/run-recovery.js +343 -0
  113. package/dist/services/runs.js +371 -74
  114. package/dist/services/runtime-budget.js +439 -0
  115. package/dist/services/runtime-command.js +43 -0
  116. package/dist/services/runtime-scope-capture.js +50 -0
  117. package/dist/services/runtime-scope-control.js +450 -0
  118. package/dist/services/runtime-scope-resume.js +565 -0
  119. package/dist/services/runtime-scope-stop.js +159 -0
  120. package/dist/services/runtime-scope-worker.js +243 -0
  121. package/dist/services/runtime-service.js +97 -0
  122. package/dist/services/schema-validation.js +10 -8
  123. package/dist/services/sessions.js +35 -69
  124. package/dist/services/stage-blocker.js +17 -7
  125. package/dist/services/stage-context.js +44 -18
  126. package/dist/services/stage-lifecycle.js +99 -102
  127. package/dist/services/stage-pause.js +84 -55
  128. package/dist/services/stage-work-graph.js +35 -0
  129. package/dist/services/usage.js +37 -93
  130. package/dist/services/vnext-code-review.js +190 -80
  131. package/dist/services/vnext-code.js +283 -248
  132. package/dist/services/vnext-execution-profile.js +5 -3
  133. package/dist/services/vnext-fanout.js +172 -17
  134. package/dist/services/vnext-merge.js +296 -105
  135. package/dist/services/vnext-plan-review.js +141 -94
  136. package/dist/services/vnext-plan.js +142 -69
  137. package/dist/services/vnext-protocolize.js +132 -57
  138. package/dist/services/vnext-specify.js +95 -65
  139. package/dist/services/work-registry.js +891 -162
  140. package/dist/services/workspace-bootstrap.js +76 -0
  141. package/dist/services/workspace-files.js +83 -0
  142. package/dist/services/worktrees.js +88 -36
  143. package/dist/shared/entity-references.js +8 -0
  144. package/dist/shared/errors.js +12 -0
  145. package/dist/storage/database.js +459 -32
  146. package/dist/storage/paths.js +17 -4
  147. package/dist/storage/work-references.js +23 -0
  148. package/dist/storage/writer-contract.js +77 -0
  149. package/dist/storage/writer-migration.js +102 -0
  150. package/package.json +24 -13
  151. package/tools/repair-paused-run-status.mjs +59 -0
@@ -0,0 +1,200 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, open, readFile, rename, unlink } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { withRunnerLock as withLock } from "./runner-lock.mjs";
5
+ import { isObservationLoss, errorRecord } from "./operation-errors.mjs";
6
+ import { operationContext } from "./operation-context.mjs";
7
+
8
+ const appendQueues = new Map();
9
+
10
+ export function canonicalJson(value) {
11
+ const normalize = (item) => {
12
+ if (Array.isArray(item)) return item.map(normalize);
13
+ if (item && typeof item === "object") return Object.fromEntries(Object.keys(item).sort().map((key) => [key, normalize(item[key])]));
14
+ return item;
15
+ };
16
+ return `${JSON.stringify(normalize(value))}\n`;
17
+ }
18
+
19
+ export function sha256(value) {
20
+ return createHash("sha256").update(value).digest("hex");
21
+ }
22
+
23
+ export function hashJson(value) { return sha256(canonicalJson(value)); }
24
+
25
+ export async function writeJsonAtomic(file, value) {
26
+ await mkdir(path.dirname(file), { recursive: true });
27
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
28
+ try {
29
+ const payload = await open(temporary, "wx", 0o600);
30
+ try { await payload.writeFile(`${JSON.stringify(value, null, 2)}\n`); await payload.sync(); }
31
+ finally { await payload.close(); }
32
+ await rename(temporary, file);
33
+ const directory = await open(path.dirname(file), "r");
34
+ try { await directory.sync(); }
35
+ finally { await directory.close(); }
36
+ } finally { await unlink(temporary).catch(error => { if (error.code !== "ENOENT") throw error; }); }
37
+ }
38
+
39
+ export async function readJsonLines(file, label = "JSONL") {
40
+ try {
41
+ const source = await readFile(file, "utf8");
42
+ return source.split("\n").filter(Boolean).map((line, index) => {
43
+ try { return JSON.parse(line); }
44
+ catch { throw new Error(`invalid ${label} record at ${file}:${index + 1}`); }
45
+ });
46
+ } catch (error) {
47
+ if (error?.code === "ENOENT") return [];
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ export async function readEvents(file) {
53
+ const records = await readJsonLines(file, "runner event");
54
+ return records.map((record, index) => {
55
+ try { return validateRunnerEvent(record); }
56
+ catch { throw new Error(`invalid runner event at ${file}:${index + 1}`); }
57
+ });
58
+ }
59
+
60
+ export function validateRunnerEvent(value) {
61
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.specversion !== "1.0" || typeof value.id !== "string" || typeof value.source !== "string" || typeof value.type !== "string" || typeof value.time !== "string" || !value.data || typeof value.data !== "object" || !Number.isInteger(value.data.sequence) || value.data.sequence < 1) {
62
+ throw new Error("invalid normalized runner event");
63
+ }
64
+ return value;
65
+ }
66
+
67
+ export async function appendEvent(file, input) {
68
+ const prior = appendQueues.get(file) ?? Promise.resolve();
69
+ const current = prior.then(() => withLock(file, async () => {
70
+ const events = await readEvents(file);
71
+ // Internal callers can validate a dispatch or bind its generation under
72
+ // the same cross-process lock that orders cancellation and completion.
73
+ if (input.deduplicate && input.id) { const existing = events.find(event => event.id === input.id); if (existing) return existing; }
74
+ if (input.beforeAppend?.(events) === false) return null;
75
+ const sequence = events.length + 1;
76
+ const event = {
77
+ specversion: "1.0",
78
+ id: input.id ?? `EVT-${randomUUID()}`,
79
+ source: input.source,
80
+ type: input.type,
81
+ time: input.time ?? new Date().toISOString(),
82
+ subject: input.subject ?? "runner",
83
+ datacontenttype: "application/json",
84
+ runid: input.runId,
85
+ executionid: input.executionId,
86
+ traceid: input.traceId,
87
+ spanid: input.spanId ?? randomUUID(),
88
+ ...(input.parentSpanId ? { parentspanid: input.parentSpanId } : {}),
89
+ data: { sequence, ...input.data }
90
+ };
91
+ const context = operationContext.getStore();
92
+ if (context?.eventsFile === file && context.executionId === input.executionId && context.operationId && input.executionId) {
93
+ event.data.execution_operation_id ??= context.operationId;
94
+ }
95
+ validateRunnerEvent(event);
96
+ await mkdir(path.dirname(file), { recursive: true });
97
+ const handle = await open(file, "a");
98
+ try { await handle.write(`${JSON.stringify(event)}\n`); await handle.sync(); }
99
+ finally { await handle.close(); }
100
+ return event;
101
+ }));
102
+ const tail = current.catch(() => {}); appendQueues.set(file, tail);
103
+ try { return await current; }
104
+ finally { if (appendQueues.get(file) === tail) appendQueues.delete(file); }
105
+ }
106
+
107
+ export function reduceEvents(events) {
108
+ const operations = new Map();
109
+ let state = "planned";
110
+ for (const event of events) {
111
+ const data = event?.data ?? {};
112
+ if (["dev.dd.eval.planned", "dev.dd.eval.completed"].includes(event.type) && typeof data.state === "string") state = data.state;
113
+ if (typeof data.operation_id !== "string") continue;
114
+ const prior = operations.get(data.operation_id) ?? { id: data.operation_id, requested: false, started: false, terminal: null };
115
+ if (event.type.endsWith(".requested")) prior.requested = true;
116
+ if (event.type.endsWith(".started")) prior.started = true;
117
+ if (event.type.endsWith(".observation_lost")) prior.observation_lost = data.error ?? { code: "observation_lost" };
118
+ if (event.type.endsWith(".completed") || event.type.endsWith(".failed") || event.type.endsWith(".cancelled")) {
119
+ const terminal = event.type.split(".").at(-1);
120
+ const result = terminal === "completed" ? data.result : { error: data.error ?? null };
121
+ const resultHash = hashJson({ terminal, result });
122
+ if (prior.terminal && (prior.terminal !== terminal || prior.result_hash !== resultHash)) {
123
+ throw Object.assign(new Error(`runner operation ${data.operation_id} has conflicting terminal events`), { code: "journal_conflict", operation_id: data.operation_id });
124
+ }
125
+ prior.terminal = terminal;
126
+ prior.result = result;
127
+ prior.result_hash = resultHash;
128
+ }
129
+ operations.set(data.operation_id, prior);
130
+ }
131
+ return { state, operations: Object.fromEntries([...operations].map(([id, value]) => [id, value])) };
132
+ }
133
+
134
+ export async function recordOperation({ eventsFile, source, runId, executionId, traceId, operationId, operation, subject = "runner", action }) {
135
+ const existing = await withLock(`${eventsFile}.${operationId.replace(/[^a-zA-Z0-9._-]/g, "_")}`, async () => {
136
+ const current = reduceEvents(await readEvents(eventsFile)).operations[operationId];
137
+ if (current?.terminal === "completed") return current;
138
+ if (current?.terminal) throw Object.assign(new Error(`operation ${operationId} is already ${current.terminal}; use explicit recovery instead of replaying it`), { code: "operation_terminal", operation_id: operationId, terminal: current.terminal });
139
+ if (current?.started && !current.terminal) {
140
+ const error = new Error(current.observation_lost
141
+ ? `operation ${operationId} has no confirmed outcome; reconcile its provider state before retrying`
142
+ : `operation ${operationId} is already in progress; reconcile before retrying`);
143
+ error.code = current.observation_lost ? "operation_observation_lost" : "operation_in_progress";
144
+ error.operation_id = operationId;
145
+ throw error;
146
+ }
147
+ await appendEvent(eventsFile, { source, runId, executionId, traceId, subject, type: "dev.dd.eval.operation.requested", data: { operation_id: operationId, operation, status: "requested" } });
148
+ await appendEvent(eventsFile, { source, runId, executionId, traceId, subject, type: "dev.dd.eval.operation.started", data: { operation_id: operationId, operation, status: "started" } });
149
+ return null;
150
+ });
151
+ if (existing) return { reused: true, operation_id: operationId, result: existing.result };
152
+ try {
153
+ const result = await operationContext.run({ operationId, eventsFile, runId, executionId }, action);
154
+ await completeOperation({ eventsFile, source, runId, executionId, traceId, subject, operationId, operation, result });
155
+ return { reused: false, operation_id: operationId, result };
156
+ } catch (error) {
157
+ if (isObservationLoss(error)) await observationLost({ eventsFile, source, runId, executionId, traceId, subject, operationId, operation, error });
158
+ else await failOperation({ eventsFile, source, runId, executionId, traceId, subject, operationId, operation, error });
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ export async function completeOperation({ eventsFile, source, runId, executionId, traceId, subject = "runner", operationId, operation, result }) {
164
+ return await settleOperation({ eventsFile, source, runId, executionId, traceId, subject, operationId, operation, terminal: "completed", result });
165
+ }
166
+
167
+ export async function failOperation({ eventsFile, source, runId, executionId, traceId, subject = "runner", operationId, operation, error }) {
168
+ return await settleOperation({ eventsFile, source, runId, executionId, traceId, subject, operationId, operation, terminal: "failed", result: { error: errorRecord(error) } });
169
+ }
170
+
171
+ /** A lost observer response is deliberately non-terminal: replay could duplicate a provider Turn. */
172
+ export async function observationLost({ eventsFile, source, runId, executionId, traceId, subject = "runner", operationId, operation, error }) {
173
+ return await withLock(`${eventsFile}.${operationId.replace(/[^a-zA-Z0-9._-]/g, "_")}`, async () => {
174
+ const existing = reduceEvents(await readEvents(eventsFile)).operations[operationId];
175
+ if (existing?.terminal) return { reused: true, operation_id: operationId, result: existing.result };
176
+ if (!existing?.started) throw Object.assign(new Error(`operation ${operationId} has no started receipt`), { code: "operation_not_started" });
177
+ if (existing.observation_lost) return { reused: true, operation_id: operationId, observation_lost: existing.observation_lost };
178
+ const recorded = errorRecord(error);
179
+ await appendEvent(eventsFile, { source, runId, executionId, traceId, subject, type: "dev.dd.eval.operation.observation_lost", data: { operation_id: operationId, operation, status: "observation_lost", error: recorded } });
180
+ return { reused: false, operation_id: operationId, observation_lost: recorded };
181
+ });
182
+ }
183
+
184
+ async function settleOperation({ eventsFile, source, runId, executionId, traceId, subject, operationId, operation, terminal, result }) {
185
+ return await withLock(`${eventsFile}.${operationId.replace(/[^a-zA-Z0-9._-]/g, "_")}`, async () => {
186
+ const existing = reduceEvents(await readEvents(eventsFile)).operations[operationId];
187
+ const expectedHash = hashJson({ terminal, result });
188
+ if (existing?.terminal) {
189
+ if (existing.terminal === terminal && existing.result_hash === expectedHash) return { reused: true, operation_id: operationId, result: existing.result };
190
+ throw Object.assign(new Error(`runner operation ${operationId} has conflicting terminal result`), { code: "journal_conflict", operation_id: operationId });
191
+ }
192
+ if (!existing?.started) throw Object.assign(new Error(`operation ${operationId} has no started receipt`), { code: "operation_not_started" });
193
+ const type = terminal === "completed" ? "dev.dd.eval.operation.completed" : "dev.dd.eval.operation.failed";
194
+ const data = terminal === "completed"
195
+ ? { operation_id: operationId, operation, status: terminal, result }
196
+ : { operation_id: operationId, operation, status: terminal, error: result.error };
197
+ await appendEvent(eventsFile, { source, runId, executionId, traceId, subject, type, data });
198
+ return { reused: false, operation_id: operationId, result };
199
+ });
200
+ }
@@ -0,0 +1,42 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, rmdirSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ function release(lock, owner) {
6
+ try { unlinkSync(path.join(lock, owner)); } catch (error) { if (error.code === "ENOENT") return; throw error; }
7
+ // Never recursively remove a directory: another owner may already have acquired it.
8
+ try { rmdirSync(lock); } catch (error) { if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code)) throw error; }
9
+ }
10
+
11
+ function reclaimDeadOwner(lock) {
12
+ let entries;
13
+ try { entries = readdirSync(lock); } catch (error) { if (error.code === "ENOENT") return; throw error; }
14
+ if (entries.length !== 1 || !/^owner-.*\.json$/.test(entries[0])) return;
15
+ let owner;
16
+ try { owner = JSON.parse(readFileSync(path.join(lock, entries[0]), "utf8")); } catch { return; }
17
+ if (!Number.isInteger(owner.pid) || owner.pid <= 0) return;
18
+ try { process.kill(owner.pid, 0); }
19
+ catch (error) { if (error.code === "ESRCH") release(lock, entries[0]); }
20
+ // A live/reused PID or unknown owner is conservative: age never proves death.
21
+ }
22
+
23
+ export async function withRunnerLock(file, action, { timeoutMs = 30_000 } = {}) {
24
+ const lock = `${file}.lock`;
25
+ const owner = `owner-${process.pid}-${randomUUID()}.json`;
26
+ const deadline = performance.now() + timeoutMs;
27
+ for (;;) {
28
+ let acquired = false;
29
+ try { mkdirSync(lock); acquired = true; }
30
+ catch (error) { if (error.code !== "EEXIST") throw error; }
31
+ if (acquired) {
32
+ // No await between acquisition and identifying the owner.
33
+ try { writeFileSync(path.join(lock, owner), JSON.stringify({ pid: process.pid }), { flag: "wx" }); }
34
+ catch (error) { release(lock, owner); throw error; }
35
+ try { return await action(); }
36
+ finally { release(lock, owner); }
37
+ }
38
+ reclaimDeadOwner(lock);
39
+ if (performance.now() >= deadline) throw Object.assign(new Error(`runner lock has a live or unconfirmed owner: ${lock}; inspect its owner before recovery`), { code: "runner_lock_timeout" });
40
+ await new Promise((resolve) => setTimeout(resolve, 10));
41
+ }
42
+ }
@@ -0,0 +1,30 @@
1
+ import { errorRecord } from "./operation-errors.mjs";
2
+
3
+ /** Diagnostic failure never prevents cancellation of already owned Sessions.
4
+ * Only a fresh successful observation can establish settlement. */
5
+ export async function waitForSettlement({ observe, cancel, knownSessions = () => [], timeoutMs = 5_000 }) {
6
+ const deadline = performance.now() + timeoutMs;
7
+ const cancelled = new Set(), errors = [];
8
+ if (cancel) for (const id of knownSessions()) {
9
+ cancelled.add(id);
10
+ try { await cancel(id); } catch (error) { errors.push({ phase: "cancel", session_id: id, ...errorRecord(error) }); }
11
+ }
12
+ for (;;) {
13
+ let state;
14
+ try { state = await observe(); }
15
+ catch (error) {
16
+ if (!cancel) throw error;
17
+ errors.push({ phase: "observe", ...errorRecord(error) });
18
+ state = { sessions: knownSessions(), active: true, observation_error: errorRecord(error) };
19
+ }
20
+ if (!state.active && state.sessions.length === 0) return { ...state, cleanup_errors: errors };
21
+ // The observation budget must not skip a newly discovered owned target.
22
+ // Attempt its cancellation once, then leave physical settlement pending.
23
+ for (const id of state.sessions) if (cancel && !cancelled.has(id)) {
24
+ cancelled.add(id);
25
+ try { await cancel(id); } catch (error) { errors.push({ phase: "cancel", session_id: id, ...errorRecord(error) }); }
26
+ }
27
+ if (!cancel || performance.now() >= deadline) throw Object.assign(new Error("daemon still owns an unsettled Session tree"), { code: "tree_not_settled", details: { ...state, cleanup_errors: errors } });
28
+ await new Promise(resolve => setTimeout(resolve, 50));
29
+ }
30
+ }
@@ -0,0 +1,17 @@
1
+ export interface ToolEvent {
2
+ session?: { harness_id: string; session_id: string };
3
+ call_id?: string;
4
+ status?: string;
5
+ name?: string;
6
+ reason?: string;
7
+ [key: string]: unknown;
8
+ }
9
+ export class ToolObservations {
10
+ calls: Map<string, ToolEvent>;
11
+ gaps: Set<string>;
12
+ add(event: ToolEvent | null): void;
13
+ summary(sessionId?: string, cursor?: Set<string>): { total: number; failures: number; pending: number; by_tool: Record<string, number>; completeness: string; sessions: Array<{ session: { harness_id: string; session_id: string }; total: number; failures: number; pending: number; by_tool: Record<string, number> }>; reasons: string[] };
14
+ cursor(): Set<string>;
15
+ }
16
+ export function codexToolObservation(record: Record<string, unknown>, sessionId: string): ToolEvent | null;
17
+ export function replayToolJournal(file: string, options: { harness: string; rootSessionId: string; adapterSessionId?: string }, reducer?: ToolObservations): ToolObservations;
@@ -0,0 +1,153 @@
1
+ import fs from "node:fs";
2
+
3
+ /** Canonical physical-session tool accounting. Native decoding lives below;
4
+ * consumers never have to inspect provider envelopes or aggregate tokens. */
5
+ export class ToolObservations {
6
+ constructor() { this.calls = new Map(); this.gaps = new Set(); }
7
+ add(event) {
8
+ if (!event) return;
9
+ if (event.reason) { this.gaps.add(event.reason); return; }
10
+ const { harness_id, session_id } = event.session ?? {};
11
+ if (!harness_id || !session_id || !event.call_id) { this.gaps.add("tool_identity_missing"); return; }
12
+ const key = JSON.stringify([harness_id, session_id, event.epoch ?? "", event.call_id]);
13
+ const old = this.calls.get(key);
14
+ // Replay and late starts cannot undo a terminal outcome. Conflicting
15
+ // terminal facts remain visible rather than silently choosing success.
16
+ const terminal = value => ["completed", "failed"].includes(value);
17
+ if (old && terminal(old.status) && terminal(event.status) && old.status !== event.status) this.gaps.add("conflicting_tool_outcomes");
18
+ this.calls.set(key, { ...old, ...event, name: event.name ?? old?.name ?? "unknown",
19
+ status: old?.status === "failed" || event.status === "failed" ? "failed" : terminal(old?.status) ? old.status : event.status ?? old?.status ?? "unknown" });
20
+ }
21
+ summary(sessionId, cursor) {
22
+ const by_tool = {}, sessions = new Map(), reasons = new Set(this.gaps); let total = 0, failures = 0, pending = 0;
23
+ for (const [key, value] of this.calls) {
24
+ if (sessionId !== undefined && value.session.session_id !== sessionId || cursor?.has(key)) continue;
25
+ const id = JSON.stringify(value.session);
26
+ if (value.name === "unknown") reasons.add("tool_start_missing");
27
+ const session = sessions.get(id) ?? { session: value.session, parent_session: value.parent_session ?? null, total: 0, failures: 0, pending: 0, by_tool: {} };
28
+ total++; session.total++; by_tool[value.name] = (by_tool[value.name] ?? 0) + 1; session.by_tool[value.name] = (session.by_tool[value.name] ?? 0) + 1;
29
+ if (value.status === "failed") { failures++; session.failures++; }
30
+ if (!["completed", "failed"].includes(value.status)) { pending++; session.pending++; }
31
+ sessions.set(id, session);
32
+ }
33
+ return { schema_id: "dd-flow/tool-summary@1", scope: "physical_sessions", measurement: "observed_invocations", origin: "native_tool_events", aggregation: cursor ? "delta" : "cumulative", total, failures, pending, by_tool,
34
+ sessions: [...sessions.values()], completeness: reasons.size ? "partial" : "complete", outcome_completeness: pending ? "partial" : "complete", reasons: [...reasons] };
35
+ }
36
+ cursor() { return new Set(this.calls.keys()); }
37
+ }
38
+
39
+ /** For adapters whose raw transcript is not portable, retain only normalized
40
+ * tool facts in the existing owned journal. Repeated snapshots are idempotent. */
41
+ export function journalToolObservations(file, events, previous) {
42
+ const batch = new ToolObservations();
43
+ for (const event of events) batch.add(event);
44
+ const records = [...batch.calls.values(), ...[...batch.gaps].map(reason => ({ reason }))];
45
+ for (const event of records) {
46
+ const key = JSON.stringify([event.session, event.epoch, event.call_id, event.reason]);
47
+ const encoded = JSON.stringify(event);
48
+ if (previous.get(key) === encoded) continue;
49
+ fs.appendFileSync(file, `${JSON.stringify({ kind: "tool_observation", schema_id: "dd-flow/tool-observation@1", observed_at: new Date().toISOString(), event })}\n`, { mode: 0o600 });
50
+ previous.set(key, encoded);
51
+ }
52
+ }
53
+
54
+ const identity = (harness_id, session_id) => ({ harness_id, session_id });
55
+ const status = value => ["error", "failed", "ERROR"].includes(value) ? "failed" : ["completed", "success", "DONE"].includes(value) ? "completed" : "running";
56
+ export function zcodePhysicalSession(update, rootSessionId) {
57
+ const native = update?._meta?.zcodeRuntime ?? {};
58
+ if (native.source === "subagent" && !native.childSessionId) return null;
59
+ return native.childSessionId ?? rootSessionId;
60
+ }
61
+
62
+ export function acpToolObservation(message, { harness = "zcode-acp", rootSessionId, adapterSessionId } = {}) {
63
+ if (!["session/update", "_x.ai/session_notification"].includes(message?.method)) return null;
64
+ const update = message.params?.update;
65
+ if (!["tool_call", "tool_call_update"].includes(update?.sessionUpdate)) return null;
66
+ const transport = message.params?.sessionId;
67
+ if (adapterSessionId && transport !== adapterSessionId) return null;
68
+ const root = rootSessionId ?? transport;
69
+ const session = harness === "zcode-acp" ? zcodePhysicalSession(update, root) : root;
70
+ if (!session || !update.toolCallId) return { reason: "tool_identity_missing" };
71
+ return { session: identity(harness, session), parent_session: session !== root ? identity(harness, root) : null,
72
+ transport_session_id: transport, call_id: update.toolCallId, name: update._meta?.["x.ai/tool"]?.name ?? update._meta?.claudeCode?.toolName ?? update.title?.split(":", 1)[0], status: status(update.status) };
73
+ }
74
+
75
+ export function droidToolObservations(record, sessionId) {
76
+ return (Array.isArray(record.message?.content) ? record.message.content : []).flatMap(block => {
77
+ if (block.type === "tool_use") return [{ session: identity("droid-cli", sessionId), call_id: block.id, name: block.name, status: "running" }];
78
+ if (block.type === "tool_result") return [{ session: identity("droid-cli", sessionId), call_id: block.tool_use_id, status: block.is_error ? "failed" : "completed" }];
79
+ return [];
80
+ });
81
+ }
82
+
83
+ export function codexToolObservation(record, sessionId) {
84
+ if (record.type !== "response_item") return null;
85
+ const p = record.payload ?? {};
86
+ if (["function_call", "custom_tool_call"].includes(p.type)) return { session: identity("codex-desktop", sessionId), call_id: p.call_id, name: p.name, status: "running" };
87
+ if (["function_call_output", "custom_tool_call_output"].includes(p.type)) {
88
+ return { session: identity("codex-desktop", sessionId), call_id: p.call_id, status: containsFailure(p) ? "failed" : "completed" };
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function containsFailure(value) {
94
+ if (Array.isArray(value)) return value.some(containsFailure);
95
+ if (typeof value === "string") {
96
+ if (/(?:^|\n)exit=(?:[1-9]\d*)\b|Process exited with code [1-9]\d*/.test(value)) return true;
97
+ if (/^[{[]/.test(value.trim())) { try { return containsFailure(JSON.parse(value)); } catch {} }
98
+ return false;
99
+ }
100
+ if (!value || typeof value !== "object") return false;
101
+ if (value.ok === false || value.is_error === true || value.isError === true || value.error === true || typeof value.error === "string") return true;
102
+ if (typeof value.exit_code === "number" && value.exit_code !== 0) return true;
103
+ return Object.values(value).some(containsFailure);
104
+ }
105
+
106
+ export function openCodeToolObservation(part, sessionId, messageId) {
107
+ if (part.type !== "tool") return null;
108
+ return { session: identity("opencode-server", part.sessionID ?? sessionId), call_id: part.callID ?? part.id,
109
+ epoch: messageId, name: part.tool, status: status(part.state?.status) };
110
+ }
111
+
112
+ export function agyToolObservation(step, rootSessionId) {
113
+ if (step.step_type !== "tool") return null;
114
+ const sessionId = step.conversation_id ?? rootSessionId;
115
+ return { session: identity("antigravity-cli", sessionId), parent_session: sessionId !== rootSessionId ? identity("antigravity-cli", rootSessionId) : null,
116
+ call_id: step.step_id ?? step.stepId ?? step.id ?? step.tool_call_id ?? step.toolCallId ?? (Number.isInteger(step.step_index) ? String(step.step_index) : null),
117
+ name: step.tool_name ?? step.tool_info?.name, status: step.tool_info?.error ? "failed" : status(step.state) };
118
+ }
119
+
120
+ /** Read-only replay, shared by accounting and offline diagnosis. A journal
121
+ * carries physical identities; an ACP connection identity never crosses out. */
122
+ export function replayToolJournal(file, options, reducer = new ToolObservations()) {
123
+ let bytes;
124
+ try { bytes = fs.readFileSync(file, "utf8"); } catch (error) { reducer.gaps.add(error.code === "ENOENT" ? "journal_missing" : "journal_unreadable"); return reducer; }
125
+ const lines = bytes.split("\n"); let session = options.rootSessionId, transport = options.adapterSessionId;
126
+ for (const [index, line] of lines.entries()) {
127
+ if (!line.trim()) continue;
128
+ let e; try { e = JSON.parse(line); } catch { reducer.gaps.add(index === lines.length - 1 ? "journal_partial_tail" : "journal_invalid_record"); continue; }
129
+ const add = event => { if (event) reducer.add({ ...event, source: { journal: file, line: index + 1 }, observed_at: e.observed_at ?? e.timestamp ?? e.at ?? e.ts ?? null }); };
130
+ if (e.kind === "tool_observation" && e.schema_id === "dd-flow/tool-observation@1") { add(e.event); continue; }
131
+ if (e.kind === "notification_failure") reducer.gaps.add("notification_failure");
132
+ const m = e.payload ?? e;
133
+ if (options.harness === "zcode-acp" || options.harness === "grok-acp") {
134
+ if (m.method === "zcode/session/allocated" && m.params?.provider_session_id === session) transport = m.params.adapter_session_id;
135
+ if (e.kind === "inbound" || m.method) add(acpToolObservation(m, { ...options, rootSessionId: session, adapterSessionId: transport }));
136
+ } else if (options.harness === "codex-desktop") {
137
+ if (e.type === "session_meta" && e.payload?.id !== session) { reducer.gaps.add("session_identity_mismatch"); break; }
138
+ add(codexToolObservation(e, session));
139
+ } else if (options.harness === "droid-cli") {
140
+ if (e.type === "session_start" && e.id !== session) { reducer.gaps.add("session_identity_mismatch"); break; }
141
+ for (const event of droidToolObservations(e, session)) add(event);
142
+ } else if (options.harness === "antigravity-cli") {
143
+ const native = m.event ?? e;
144
+ if (native.event === "step_update") add(agyToolObservation(native.step_update ?? {}, session));
145
+ } else if (options.harness === "opencode-server") {
146
+ // Native SSE message.part.updated, retained in the adapter journal.
147
+ const event = m.event ?? m;
148
+ const part = event.properties?.part;
149
+ if (part) add(openCodeToolObservation(part, session, part.messageID));
150
+ } else reducer.gaps.add("tool_observation_unsupported");
151
+ }
152
+ return reducer;
153
+ }
@@ -0,0 +1,54 @@
1
+ /** Accounting policy over native evidence; the bridge only exposes native RPCs. */
2
+ export function normalizeZcodeUsage(usage, read) {
3
+ const result = { ...usage, requestUsageStatus: "unavailable" };
4
+ const totals = { requestTotalTokens: 0, requestInputTokens: 0, requestOutputTokens: 0,
5
+ requestReasoningTokens: 0, requestCacheCreationTokens: 0, requestCacheReadTokens: 0, requestCount: 0 };
6
+ const seen = new Map();
7
+ let incomplete = !Array.isArray(read?.messages);
8
+ for (const message of Array.isArray(read?.messages) ? read.messages : []) {
9
+ const info = message?.info;
10
+ if (info?.role !== "assistant") continue;
11
+ // Native history includes system timeline markers with role=assistant and
12
+ // zero token placeholders, but no model request (and no total counter).
13
+ if (info.semantics?.origin === "system" && info.semantics?.kind === "timeline_event") continue;
14
+ if (!info.tokens) { incomplete = true; continue; }
15
+ const tokens = info.tokens;
16
+ const id = info.messageId ?? info.id ?? message.id;
17
+ if (id) {
18
+ const signature = JSON.stringify(tokens);
19
+ if (seen.has(id)) { if (seen.get(id) !== signature) incomplete = true; continue; }
20
+ seen.set(id, signature);
21
+ }
22
+ const values = [tokens.total, tokens.input, tokens.output];
23
+ if (!values.every(valid)) { incomplete = true; continue; }
24
+ totals.requestTotalTokens += tokens.total;
25
+ totals.requestInputTokens += tokens.input;
26
+ totals.requestOutputTokens += tokens.output;
27
+ for (const [key, value] of [["requestReasoningTokens", tokens.reasoning],
28
+ ["requestCacheCreationTokens", tokens.cache?.write], ["requestCacheReadTokens", tokens.cache?.read]]) {
29
+ if (value !== undefined && !valid(value)) incomplete = true;
30
+ else totals[key] += value ?? 0;
31
+ }
32
+ totals.requestCount++;
33
+ }
34
+ if (totals.requestCount && !incomplete) Object.assign(result, totals, { requestUsageStatus: "measured" });
35
+ if (incomplete) result.usageIsIncomplete = true;
36
+ return result;
37
+ }
38
+
39
+ function valid(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
40
+
41
+ export async function readZcodeUsage(bridge, sessionId) {
42
+ const usage = await bridge.request("zcode/session/usage", { sessionId });
43
+ // Compatibility with immutable @1 bridges: they already expose this projection.
44
+ if (usage?.requestUsageStatus === "measured") return usage;
45
+ try {
46
+ // Unbounded native read is intentional: accounting needs the full history,
47
+ // unlike turn liveness reads which may use messageLimit: 1.
48
+ const read = await bridge.request("zcode/session/read", { sessionId });
49
+ return normalizeZcodeUsage(usage, read);
50
+ } catch (error) {
51
+ return { ...usage, requestUsageStatus: "unavailable", usageIsIncomplete: true,
52
+ requestUsageError: { code: error.code ?? "usage_evidence_failed", message: error.message, ...(error.details ? { details: error.details } : {}) } };
53
+ }
54
+ }
@@ -7,7 +7,7 @@
7
7
  "properties": {
8
8
  "schema_id": {"const": "dd-flow/agent-profile@1"},
9
9
  "id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"},
10
- "harness": {"enum": ["codex", "zcode", "agy", "opencode", "grok"]},
10
+ "harness": {"enum": ["codex", "zcode", "agy", "opencode", "grok", "droid"]},
11
11
  "provider": {"type": "string", "minLength": 1},
12
12
  "model": {"type": "string", "minLength": 1},
13
13
  "reasoning": {"type": "string", "minLength": 1},
@@ -8,7 +8,7 @@
8
8
  "schema_id": {"const": "dd-flow/code-review-result@1"},
9
9
  "verdict": {"enum": ["pass", "findings", "blocked"]},
10
10
  "summary": {"type": "string", "minLength": 1},
11
- "aspects": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["aspect_id", "verdict", "summary", "evidence_refs"], "properties": {"aspect_id": {"type": "string", "minLength": 1}, "verdict": {"enum": ["pass", "findings", "blocked"]}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "items": {"type": "string"}}}}},
11
+ "aspects": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["aspect_id", "verdict", "summary", "evidence_refs"], "properties": {"aspect_id": {"type": "string", "minLength": 1}, "verdict": {"enum": ["pass", "findings", "blocked"]}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}}},
12
12
  "findings": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["finding_id", "aspect_id", "priority", "problem", "impact", "required_outcome", "evidence_refs", "obligation_refs"], "properties": {"finding_id": {"type": "string", "pattern": "^FIND-[0-9]{3}$"}, "aspect_id": {"type": "string", "minLength": 1}, "priority": {"enum": ["p0", "p1", "p2", "p3"]}, "problem": {"type": "string", "minLength": 1}, "impact": {"type": "string", "minLength": 1}, "required_outcome": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "obligation_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}}}
13
13
  }
14
14
  }
@@ -16,10 +16,10 @@
16
16
  "spine": {"type": "object", "additionalProperties": false, "required": ["user_outcome", "component_responsibility", "must_preserve", "non_goals", "acceptance_contribution"], "properties": {"user_outcome": {"type": "string", "minLength": 1}, "component_responsibility": {"type": "string", "minLength": 1}, "must_preserve": {"$ref": "#/$defs/strings"}, "non_goals": {"$ref": "#/$defs/stringList"}, "acceptance_contribution": {"type": "string", "minLength": 1}}},
17
17
  "obligation": {"type": "object", "additionalProperties": false, "required": ["id", "statement"], "properties": {"id": {"type": "string", "minLength": 1}, "statement": {"type": "string", "minLength": 1}}},
18
18
  "acceptance": {"type": "object", "additionalProperties": false, "required": ["criterion_id", "path", "environment", "fixtures", "cleanup", "check_refs", "expected_evidence", "proof_limits", "gate"], "properties": {"criterion_id": {"type": "string", "pattern": "^AC-[0-9]+$"}, "path": {"type": "string", "minLength": 1}, "environment": {"type": "string", "minLength": 1}, "fixtures": {"$ref": "#/$defs/stringList"}, "cleanup": {"type": "string", "minLength": 1}, "check_refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "expected_evidence": {"$ref": "#/$defs/strings"}, "proof_limits": {"$ref": "#/$defs/strings"}, "gate": {"type": "string", "minLength": 1}}},
19
- "check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release", "external"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "minLength": 1}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "ports": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"}}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}]},
19
+ "check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "minLength": 1}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "ports": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"}}, "inputs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "reuse": {"const": "deterministic"}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}]},
20
20
  "documentUpdate": {"type": "object", "additionalProperties": false, "required": ["path", "action", "owner", "reason", "baseline_sha256"], "properties": {"path": {"type": "string", "minLength": 1}, "action": {"enum": ["create", "update"]}, "owner": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}, "baseline_sha256": {"type": ["string", "null"], "pattern": "^[a-f0-9]{64}$"}}},
21
21
  "reviewFinding": {"type": "object", "additionalProperties": false, "required": ["finding_ref", "priority", "problem", "impact", "required_outcome", "evidence_refs", "obligation_refs", "decision_reason", "check_refs"], "properties": {"finding_ref": {"type": "string", "minLength": 1}, "priority": {"enum": ["p0", "p1", "p2", "p3"]}, "problem": {"type": "string", "minLength": 1}, "impact": {"type": "string", "minLength": 1}, "required_outcome": {"type": "string", "minLength": 1}, "evidence_refs": {"$ref": "#/$defs/strings"}, "obligation_refs": {"$ref": "#/$defs/strings"}, "decision_reason": {"type": "string", "minLength": 1}, "check_refs": {"$ref": "#/$defs/strings"}}},
22
- "repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_findings"]}, {"required": ["semantic_unresolved", "verification_path"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_findings": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/reviewFinding"}}, "semantic_unresolved": {"$ref": "#/$defs/strings"}, "verification_path": {"type": "string", "minLength": 1}}},
23
- "work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@2"}}}
22
+ "repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids", "verification_check_refs"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_findings"]}, {"required": ["semantic_unresolved", "verification_path"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_findings": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/reviewFinding"}}, "semantic_unresolved": {"$ref": "#/$defs/strings"}, "verification_path": {"type": "string", "minLength": 1}, "verification_check_refs": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/check"}}}},
23
+ "work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@3"}}}
24
24
  }
25
25
  }
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "dd-flow/code-work-result@2",
3
+ "$id": "dd-flow/code-work-result@3",
4
4
  "type": "object",
5
5
  "additionalProperties": false,
6
6
  "required": ["schema_id", "summary", "changed_paths", "evidence", "deviations", "blockers"],
7
7
  "properties": {
8
- "schema_id": {"const": "dd-flow/code-work-result@2"},
8
+ "schema_id": {"const": "dd-flow/code-work-result@3"},
9
9
  "summary": {"type": "string", "minLength": 1},
10
10
  "changed_paths": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
11
- "evidence": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["criterion_id", "refs"], "properties": {"criterion_id": {"type": "string", "minLength": 1}, "refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}},
11
+ "evidence": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["obligation_ref", "refs"], "properties": {"obligation_ref": {"type": "string", "minLength": 1}, "refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}},
12
12
  "deviations": {"type": "array", "items": {"type": "string", "minLength": 1}},
13
13
  "blockers": {"type": "array", "items": {"type": "string", "minLength": 1}},
14
14
  "resolutions": {"type": "array", "uniqueItems": true, "items": {"type": "object", "additionalProperties": false, "required": ["finding_ref", "summary", "evidence_refs"], "properties": {"finding_ref": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}}
@@ -20,7 +20,7 @@
20
20
  "aspect_id": {"type": "string", "minLength": 1},
21
21
  "verdict": {"enum": ["pass", "watch", "needs_changes", "blocked"]},
22
22
  "summary": {"type": "string", "minLength": 1},
23
- "evidence_refs": {"type": "array", "items": {"type": "string", "minLength": 1}},
23
+ "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
24
24
  "findings": {
25
25
  "type": "array",
26
26
  "items": {
@@ -31,7 +31,7 @@
31
31
  "finding_id": {"type": "string", "pattern": "^FIND-[0-9]{3}$"},
32
32
  "severity": {"enum": ["blocker", "high", "medium", "low", "info"]},
33
33
  "summary": {"type": "string", "minLength": 1},
34
- "evidence_refs": {"type": "array", "items": {"type": "string", "minLength": 1}}
34
+ "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}
35
35
  }
36
36
  }
37
37
  }