@deksden-com/dd-flow-cli 0.9.0-beta.156 → 0.9.0-beta.158

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 (33) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/error-output.js +11 -1
  4. package/dist/cli/input-preparation.js +32 -4
  5. package/dist/cli/run-cli.js +24 -7
  6. package/dist/harness-runtime/lib/daemon-operations.mjs +46 -11
  7. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +19 -6
  8. package/dist/harness-runtime/lib/dd-agy.mjs +19 -0
  9. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +1 -1
  10. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +1 -1
  11. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -2
  12. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +1 -1
  13. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
  14. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  15. package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -0
  16. package/dist/schemas/code-work-batch.schema.json +1 -1
  17. package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
  18. package/dist/services/codex-capacity.js +367 -0
  19. package/dist/services/controller-child-repair.js +46 -24
  20. package/dist/services/controller-fanout.js +23 -15
  21. package/dist/services/controller-result-authority.js +13 -8
  22. package/dist/services/controller-work-failure.js +198 -0
  23. package/dist/services/lifecycle-invocations.js +81 -16
  24. package/dist/services/managed-daemon-binding.js +40 -0
  25. package/dist/services/run-control.js +18 -36
  26. package/dist/services/run-controller.js +107 -22
  27. package/dist/services/vnext-code.js +3 -1
  28. package/dist/services/vnext-plan-review.js +2 -2
  29. package/dist/services/vnext-plan.js +3 -3
  30. package/dist/services/work-registry.js +59 -12
  31. package/dist/services/work-retry-context.js +55 -0
  32. package/dist/verification-components.json +3 -0
  33. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.158
4
+
5
+ ### Patch Changes
6
+
7
+ - Resolve proven explicit Work refusals through coordinator retry/block/pause/fail decisions. Retain accepted corrections and launch history, recover lost acceptance/pause acknowledgements, and keep native/provider failures fenced. Align PLAN stop conditions with accepted intent (plan 112).
8
+ - Register focused PLAN/canon verification ownership so release selection never falls back to an unrelated full gate.
9
+
10
+ ## 0.9.0-beta.157
11
+
12
+ ### Patch Changes
13
+
14
+ - b7555a7: Preserve actionable lifecycle output and actual Work inputs through preparation retries, persist refusal-to-successor links, and allow distinct same-child correction episodes without duplicate dispatch. Reconcile consistent observations of one technical pause without losing its accepted source.
15
+
16
+ Retire owned execution after native shutdown failures without fabricating native success. Preserve AGY terminal task evidence and reject capture when native inventory changes during physical observation.
17
+
3
18
  ## 0.9.0-beta.156
4
19
 
5
20
  ### Patch Changes
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.156",
4
- "cli_commit": "1a2bd0ea6d022b22e94f552f1cde1fb8e8b6689e",
5
- "built_at": "2026-10-28T07:26:47.345Z",
3
+ "cli_version": "0.9.0-beta.158",
4
+ "cli_commit": "d0da77c661fa63daf690214f177105c0c0892262",
5
+ "built_at": "2026-10-29T05:58:19.667Z",
6
6
  "built_with_canon": {
7
- "version": "4.1.3",
8
- "commit": "052ffdf0fff642a46485d2decc79bf78b1970d5e",
7
+ "version": "4.1.4",
8
+ "commit": "7cc415b3ef3ce37c9624c7cee51023e66f67cd57",
9
9
  "flow_contract": "dd-flow-canonical-2026-08",
10
- "repo_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-cp260-manifest",
11
- "memorybank_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-cp260-manifest/.memory-bank",
12
- "flow_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-cp260-manifest/.memory-bank/dd-flow",
10
+ "repo_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-cp263",
11
+ "memorybank_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-cp263/.memory-bank",
12
+ "flow_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-canon-cp263/.memory-bank/dd-flow",
13
13
  "layout": "dot_memory_bank"
14
14
  }
15
15
  }
@@ -22,8 +22,18 @@ export function writePreparationError(io, args, error) {
22
22
  io.stderr.write(renderHumanError(payload, args));
23
23
  }
24
24
  export function renderHumanError(payload, args) {
25
- const lines = [`dd-flow: ${payload.error.message}`];
25
+ const lines = [`dd-flow: ${payload.error.message}`, `code: ${payload.error.code}`];
26
26
  const details = recordObject(payload.error.details);
27
+ for (const key of ["effect", "recoverable", "outcome", "instruction", "retry_instruction", "retry_command", "handoff_required", "work_id", "recovery_instruction"]) {
28
+ if (details?.[key] !== undefined)
29
+ lines.push(`${key}: ${String(details[key])}`);
30
+ }
31
+ const continuation = recordObject(details?.continuation);
32
+ if (continuation)
33
+ for (const key of ["outcome", "instruction", "work_id", "finish_command", "resume_command", "prompt_path"]) {
34
+ if (continuation[key] !== undefined)
35
+ lines.push(`continuation.${key}: ${String(continuation[key])}`);
36
+ }
27
37
  const compatibility = recordObject(details?.compatibility);
28
38
  const remediation = recordObject(details?.remediation);
29
39
  if (compatibility) {
@@ -1,8 +1,29 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import crypto from "node:crypto";
4
+ import { AsyncLocalStorage } from "node:async_hooks";
4
5
  import { AppError, isAppError } from "../shared/errors.js";
5
6
  import { validateSchema } from "../services/schema-validation.js";
7
+ // Observe only bytes already consumed by CLI preparation; never reread a file
8
+ // in a hook or while replaying a retained outcome.
9
+ const consumedInputs = new AsyncLocalStorage();
10
+ export function recordConsumedInput(bytes) {
11
+ recordConsumedInputDigest(crypto.createHash("sha256").update(bytes).digest("hex"));
12
+ }
13
+ export function recordConsumedInputDigest(digest) { consumedInputs.getStore()?.push(digest); }
14
+ export async function withPreparedInputBinding(prepare) {
15
+ return consumedInputs.run([], async () => {
16
+ try {
17
+ return await prepare();
18
+ }
19
+ catch (error) {
20
+ const inputs = consumedInputs.getStore();
21
+ if (isAppError(error) && inputs.length)
22
+ error.details.consumed_input_sha256 = crypto.createHash("sha256").update(JSON.stringify(inputs)).digest("hex");
23
+ throw error;
24
+ }
25
+ });
26
+ }
6
27
  export function prepareError(error, input = {}) {
7
28
  if (isAppError(error)) {
8
29
  // A malformed user input is safely correctable. A broken schema registry,
@@ -61,7 +82,9 @@ export async function readBoundedUtf8Stream(stream, maxBytes) {
61
82
  }
62
83
  }
63
84
  try {
64
- return data + decoder.decode();
85
+ const text = data + decoder.decode();
86
+ recordConsumedInput(text);
87
+ return text;
65
88
  }
66
89
  catch {
67
90
  throw new AppError("validation", "Standard input must contain valid UTF-8", 2);
@@ -76,8 +99,11 @@ export function prepareFileBytes(file, label = "input file", maxBytes) {
76
99
  if (!stat.isFile()) {
77
100
  throw new AppError("input_file_not_regular", `${label} must be a readable regular file: ${absolute}`, 2, { file: absolute, label });
78
101
  }
79
- if (maxBytes === undefined)
80
- return { file: absolute, bytes: fs.readFileSync(descriptor) };
102
+ if (maxBytes === undefined) {
103
+ const bytes = fs.readFileSync(descriptor);
104
+ recordConsumedInput(bytes);
105
+ return { file: absolute, bytes };
106
+ }
81
107
  if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
82
108
  throw new AppError("usage", "Invalid input byte bound", 2);
83
109
  const oversized = () => new AppError("usage", `${label} exceeds its byte bound`, 2);
@@ -94,7 +120,9 @@ export function prepareFileBytes(file, label = "input file", maxBytes) {
94
120
  }
95
121
  if (length > maxBytes)
96
122
  throw oversized();
97
- return { file: absolute, bytes: buffer.subarray(0, length) };
123
+ const bytes = buffer.subarray(0, length);
124
+ recordConsumedInput(bytes);
125
+ return { file: absolute, bytes };
98
126
  }
99
127
  catch (error) {
100
128
  if (error?.code === "ENOENT") {
@@ -5,7 +5,7 @@ import { requiredOption, optionalOption, optionalNumber, optionalPositiveNumber
5
5
  import { resolveCommandPaths, resolveCommandProjectRoot } from "../services/command-context.js";
6
6
  import { assertAuxiliaryCommandScope, cliManagedInvocationScope, manualRecoveryInvocationScope, assertManualRecoveryInvocationTarget, currentAssignedLifecycle } from "../services/lifecycle-invocations.js";
7
7
  import path from "node:path";
8
- import { randomUUID } from "node:crypto";
8
+ import { createHash, randomUUID } from "node:crypto";
9
9
  import { quote } from "shell-quote";
10
10
  import { observeLifecycleCommand, observedLifecycleNativeEvent } from "../services/lifecycle-invocations.js";
11
11
  import { verifyCodexHookDelivery } from "../services/codex-hook-delivery.js";
@@ -40,7 +40,7 @@ import { getCanonStatus, registerCanonRoot, resolveCanonRoot, prepareCanonRegist
40
40
  import { getRuntimeStatus } from "../services/status.js";
41
41
  import { getCliVersionReport } from "../services/build-info.js";
42
42
  import { parseCommandArgs } from "../services/lifecycle-command.js";
43
- import { prepareError, prepareJsonFile, prepareJsonSchema, prepareJsonText, prepareTextFile, prepareFileBytes, readBoundedUtf8Stream } from "./input-preparation.js";
43
+ import { prepareError, prepareJsonFile, prepareJsonSchema, prepareJsonText, prepareTextFile, prepareFileBytes, readBoundedUtf8Stream, recordConsumedInputDigest, withPreparedInputBinding } from "./input-preparation.js";
44
44
  import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, openGlobalDashboard, openProtocolDashboard, refreshAllDashboards, prepareDashboardRefresh, prepareDashboardRender, prepareGlobalDashboardRender, prepareDashboardOptions, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
45
45
  import { dashboardTargetSummary, preferredDashboardCommand, relatedDashboardCommands, resolveDashboardTarget } from "../services/dashboard-targets.js";
46
46
  import { getFlowSessionStatus, prepareFlowSessionPayload, registerFlowSession, stopFlowSession, stopMergeWorker, syncFlowSessionUsage } from "../services/sessions.js";
@@ -343,6 +343,10 @@ async function runCliRequest(args, io, env) {
343
343
  ? expandLifecycleInvocationArgs(routerContext, boundInvocationArgs, invocationScope, observedOperation)
344
344
  : boundInvocationArgs;
345
345
  const retainedOutput = observedInvocation ? parseOutputOptions(expandedInvocationArgs) : null;
346
+ if (retainedOutput?.json)
347
+ output.json = true;
348
+ if (retainedOutput?.progressJsonl)
349
+ output.progressJsonl = true;
346
350
  if (retainedOutput?.responseFile)
347
351
  output.responseFile = retainedOutput.responseFile;
348
352
  let invocationArgs = retainedOutput?.args ?? expandedInvocationArgs;
@@ -728,7 +732,7 @@ function attachProgress(result, progress, output) {
728
732
  }
729
733
  return { ok: true, result, progress };
730
734
  }
731
- function renderHumanResult(args, result) {
735
+ export function renderHumanResult(args, result) {
732
736
  const [family, command] = args;
733
737
  const record = result && typeof result === "object" && !Array.isArray(result) ? result : {};
734
738
  const ok = record.ok === false ? "failed" : "ok";
@@ -796,6 +800,16 @@ function renderHumanResult(args, result) {
796
800
  if (run.run_index_path)
797
801
  lines.push(`run_index: ${String(run.run_index_path)}`);
798
802
  }
803
+ for (const key of ["outcome", "instruction", "agent_instruction", "next_action", "resume_command", "resume_command_template", "prompt_path"]) {
804
+ if (record[key] !== undefined && typeof record[key] !== "object")
805
+ lines.push(`${key}: ${String(record[key])}`);
806
+ }
807
+ for (const [name, keys] of [["next", ["finish_command"]], ["repair", ["work_id"]], ["prompt_delivery", ["path", "sha256"]], ["pause", ["user_message", "question_path", "resume_command", "resume_command_template"]]]) {
808
+ const value = recordObject(record[name]);
809
+ for (const key of keys)
810
+ if (value?.[key] !== undefined)
811
+ lines.push(`${name}.${key}: ${String(value[key])}`);
812
+ }
799
813
  appendFlowGuidanceHuman(lines, record);
800
814
  return `${lines.join("\n")}\n`;
801
815
  }
@@ -933,7 +947,7 @@ function compositionProposalRetry(context, parsed) {
933
947
  * invocation is allowed to claim execution or a stage claims its hook event. */
934
948
  async function prepareCliInput(...args) {
935
949
  try {
936
- return await prepareCliInputValue(...args);
950
+ return await withPreparedInputBinding(() => prepareCliInputValue(...args));
937
951
  }
938
952
  catch (error) {
939
953
  return prepareError(error);
@@ -3672,11 +3686,14 @@ async function readStdin(stream, maxBytes) {
3672
3686
  let data = "";
3673
3687
  if (maxBytes !== undefined)
3674
3688
  return readBoundedUtf8Stream(stream, maxBytes);
3675
- stream.setEncoding("utf8");
3689
+ const hash = createHash("sha256"), decoder = new TextDecoder("utf-8", { ignoreBOM: true });
3676
3690
  for await (const chunk of stream) {
3677
- data += chunk;
3691
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
3692
+ hash.update(bytes);
3693
+ data += decoder.decode(bytes, { stream: true });
3678
3694
  }
3679
- return data;
3695
+ recordConsumedInputDigest(hash.digest("hex"));
3696
+ return data + decoder.decode();
3680
3697
  }
3681
3698
  function refreshDashboardsAfterMutation(context, args, result, resolvedScope) {
3682
3699
  try {
@@ -9,7 +9,7 @@ import { assertDaemonOwnership, budgetObservationScope, releaseBudgetObservation
9
9
  import { processSnapshot } from "./process-snapshot.mjs";
10
10
  import { NATIVE_SHUTDOWN_CONTRACT, NATIVE_SHUTDOWN_PHASES } from "./native-children.mjs";
11
11
  import { readModelObservations, modelAttribution } from "./model-observations.mjs";
12
- import { publishOperationProgress, publishOperationWaiting, withOperationProgress } from './operation-progress.mjs';
12
+ import { publishOperationProgress, publishOperationWaiting, withOperationProgress, noteOperationProgress } from './operation-progress.mjs';
13
13
  import { prepareDaemonRequest } from "./native-request-proof.mjs";
14
14
 
15
15
  const productive = new Set(["session.create", "session.prompt", "session.start", "session.fork", "session.resume"]);
@@ -65,30 +65,50 @@ function ownedSnapshot(snapshot, rootPid) {
65
65
  const mergeProcesses = (...snapshots) => [...new Map(snapshots.flat().map(item => [`${item.pid}:${item.started}`, item])).values()];
66
66
 
67
67
  /** The same incarnation may retry remaining cleanup, never a native Turn. */
68
- export async function completeDaemonShutdown(root, state, persist, prepare, phases, reconcileResources) {
68
+ export async function completeDaemonShutdown(root, state, persist, prepare, phases, reconcileResources, { cancelTree = false } = {}) {
69
69
  const daemonId = state.daemon_id ?? state.config?.daemonId;
70
70
  let shutdown = state.shutdown;
71
71
  if (shutdown && shutdown.schema_id !== NATIVE_SHUTDOWN_CONTRACT) throw Object.assign(new Error("shutdown contract mismatch; retain historical owner evidence"), { code: "native_contract_mismatch" });
72
72
  if (!shutdown || shutdown.daemon_id !== daemonId) shutdown = { schema_id: NATIVE_SHUTDOWN_CONTRACT, daemon_id: daemonId, generation: dispatchGenerations.get(root) ?? 0, phases: {} };
73
73
  shutdown = { ...shutdown, required_phases: [...new Set([...NATIVE_SHUTDOWN_PHASES, ...phases.map(([phase]) => phase)])] };
74
74
  try {
75
- if (!shutdown.phases.execution) {
75
+ if (!shutdown.phases.execution && !shutdown.physical_only) {
76
76
  // Some native adapters close their provider inside prepare(). Observe
77
77
  // ownership first, before that close can reparent a support process.
78
78
  const before = ownedSnapshot(await processSnapshot(), state.pid);
79
79
  shutdown = { ...shutdown, generation: dispatchGenerations.get(root) ?? shutdown.generation,
80
80
  owned_processes: mergeProcesses(shutdown.owned_processes ?? [], before), inventory_complete: state.pid === process.pid };
81
81
  await persist({ shutdown });
82
- const result = await prepare();
82
+ let result;
83
+ try { result = await prepare(); }
84
+ catch (error) {
85
+ if (!cancelTree) throw error;
86
+ // An explicit whole-tree stop owns physical cleanup even when a dead
87
+ // native endpoint cannot acknowledge idle. Never fabricate execution.
88
+ shutdown = { ...shutdown, physical_only: true, preparation_error: errorRecord(error) };
89
+ result = { clean: false, settled: false, native_settled: false,
90
+ native_resources: { entries: [...(shutdown.result?.native_resources?.entries ?? []), ...(state.native_resources_entries ?? [])] } };
91
+ }
92
+ if (cancelTree && (result?.settled !== true || result?.native_settled !== true)) {
93
+ // Some adapters return the failed native observation after cancelling
94
+ // their provider instead of throwing it. The endpoint is no more
95
+ // available in that case; the same physical-only path applies.
96
+ shutdown = { ...shutdown, physical_only: true };
97
+ result = { ...result, clean: false, settled: false, native_settled: false,
98
+ native_resources: { ...result?.native_resources, entries: [...(shutdown.result?.native_resources?.entries ?? []),
99
+ ...(result?.native_resources?.entries ?? []), ...(state.native_resources_entries ?? [])] } };
100
+ }
83
101
  commitDaemonShutdown(root);
84
102
  // Compute ancestry within each snapshot, not across recycled parent PIDs.
85
103
  const resources = mergeProcesses(shutdown.owned_processes, ownedSnapshot(await processSnapshot(), state.pid));
86
- shutdown = { ...shutdown, phases: { execution: true }, owned_processes: resources,
104
+ shutdown = { ...shutdown, phases: { ...shutdown.phases, execution: !shutdown.physical_only }, owned_processes: resources,
87
105
  inventory_complete: state.pid === process.pid, tree_observed_at: new Date().toISOString(), preparation_clean: result?.clean !== false, result: result ?? {} };
88
106
  await persist({ shutdown, shutdown_state: "stopping" });
107
+ await noteOperationProgress('settlement_started', { cleanup_phase: 'prepare', physical_only: shutdown.physical_only === true });
89
108
  }
90
109
  commitDaemonShutdown(root);
91
- if (reconcileResources && !shutdown.phases.owned_resources) {
110
+ if (reconcileResources && !shutdown.physical_only && !shutdown.phases.owned_resources) {
111
+ try {
92
112
  const revision = state.native_execution_revision, generation = dispatchGenerations.get(root) ?? shutdown.generation;
93
113
  // A fresh attempt cannot reuse a previous inventory while reconciliation
94
114
  // is in flight or after the new attempt fails.
@@ -108,6 +128,15 @@ export async function completeDaemonShutdown(root, state, persist, prepare, phas
108
128
  native_resource_reconciliation: { daemon_id: daemonId, dispatch_generation: generation, native_revision: revision },
109
129
  result: { ...shutdown.result, native_resources: resources } };
110
130
  await persist({ shutdown });
131
+ await noteOperationProgress('native_progress', { cleanup_phase: 'native_resources_reconciled' });
132
+ } catch (error) {
133
+ if (!cancelTree || (state.daemon_id ?? state.config?.daemonId) !== daemonId) throw error;
134
+ shutdown = { ...shutdown, physical_only: true, reconciliation_error: errorRecord(error),
135
+ result: { ...shutdown.result, clean: false, settled: false, native_settled: false,
136
+ native_resources: { ...shutdown.result?.native_resources,
137
+ entries: [...(shutdown.result?.native_resources?.entries ?? []), ...(state.native_resources_entries ?? [])] } } };
138
+ await persist({ shutdown });
139
+ }
111
140
  }
112
141
  for (const [phase, action] of phases) {
113
142
  if (phase === "daemon_resource" && !shutdown.phases.owned_resources) {
@@ -120,9 +149,14 @@ export async function completeDaemonShutdown(root, state, persist, prepare, phas
120
149
  const remaining = await processSnapshot();
121
150
  if ((shutdown.owned_processes ?? []).some(retained => remaining.some(item => item.pid === retained.pid && item.started === retained.started && !item.zombie))) throw Object.assign(new Error("owned resources remain live"), { code: "native_resources_unproven" });
122
151
  const nativeResources = shutdown.result?.native_resources;
123
- const resourceGap = nativeResources?.entries?.some(resource => resource.observation_conflict === true || resource.state !== "stopped"
124
- && !(resource.classification === "required_work" && shutdown.result?.native_settled === true && shutdown.result?.settled === true)
125
- && !(shutdown.owned_processes ?? []).some(item => resource.pid === item.pid && resource.started === item.started));
152
+ const resourceGap = nativeResources?.entries?.some(resource => {
153
+ if (resource.observation_conflict === true) return true;
154
+ if (resource.pid !== undefined) return !Number.isSafeInteger(resource.pid) || resource.pid <= 0
155
+ || typeof resource.started !== 'string' || !resource.started
156
+ || remaining.some(item => item.pid === resource.pid && !item.zombie);
157
+ return resource.state !== "stopped"
158
+ && !(resource.classification === "required_work" && shutdown.result?.native_settled === true && shutdown.result?.settled === true);
159
+ });
126
160
  shutdown = { ...shutdown, phases: { ...shutdown.phases, owned_resources: !resourceGap },
127
161
  resource_retirement: { confirmed: !resourceGap, reason: resourceGap ? "native_resource_identity_unproven" : "owned_processes_retired" } };
128
162
  await persist({ shutdown });
@@ -132,14 +166,15 @@ export async function completeDaemonShutdown(root, state, persist, prepare, phas
132
166
  if (resourceGap) throw Object.assign(new Error("native resource retirement remains unproven"), { code: "native_resources_unproven" });
133
167
  }
134
168
  if (shutdown.phases[phase]) continue;
135
- await action();
169
+ await action({ physicalOnly: shutdown.physical_only === true });
136
170
  shutdown = { ...shutdown, phases: { ...shutdown.phases, [phase]: true } };
137
171
  await persist({ shutdown });
172
+ await noteOperationProgress('native_progress', { cleanup_phase: phase });
138
173
  }
139
174
  const publishedRevision = state.native_execution_revision;
140
175
  const sameRevision = publishedRevision === undefined || publishedRevision === shutdown.result?.native_settlement?.revision;
141
176
  const nativeSettled = sameRevision && state.active_tree === false && !state.unclaimed_activity && shutdown.result?.settled === true && shutdown.result?.native_settled === true && shutdown.result?.native_settlement?.confirmed !== false;
142
- const physicalStopped = shutdown.required_phases.every(phase => shutdown.phases[phase] === true);
177
+ const physicalStopped = shutdown.required_phases.filter(phase => phase !== "execution").every(phase => shutdown.phases[phase] === true);
143
178
  // Cleanup retries/joining controls advance the control fence, not the old
144
179
  // native Turn. Publish the current control generation with retained phases.
145
180
  shutdown = { ...shutdown, generation: dispatchGenerations.get(root) ?? shutdown.generation };
@@ -25,11 +25,11 @@ import { agyWorkspaceHooks } from "./agy-hooks.mjs";
25
25
  import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
26
26
  import path from "node:path";
27
27
  import { observeModel } from "./model-observations.mjs";
28
- import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, agyModelArgs, assertProfile, executable, observedProfile, runAgy, usageSnapshot, managedAgyEnv } from "./dd-agy.mjs";
28
+ import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, agyErrorDiagnostic, agyModelArgs, assertProfile, executable, observedProfile, runAgy, usageSnapshot, managedAgyEnv } from "./dd-agy.mjs";
29
29
  import { agyTerminalEnvelope } from "./agy-terminal-envelope.mjs";
30
30
  import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
31
31
  import { retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
32
- import { readNativeLines, streamNativeDiagnostics, nativeObservationError, admitNativeRequestBuffer } from "./native-streams.mjs";
32
+ import { readNativeLines, nativeObservationError, admitNativeRequestBuffer } from "./native-streams.mjs";
33
33
 
34
34
  const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
35
35
  class DaemonError extends AgyError {}
@@ -400,7 +400,12 @@ export class Runtime {
400
400
  const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
401
401
  const observationFailure = error => { this.drainingError ??= error; readyReject(error); this.active?.reject(error); };
402
402
  this.nativeDrain = readNativeLines(this.child.stdout, line => { this.buffer = `${line}\n`; return this.draining = this.drain(readyResolve, readyReject); }, observationFailure);
403
- this.diagnostics = streamNativeDiagnostics(this.child.stderr, chunk => this.journal("stderr", { text: chunk }), observationFailure);
403
+ const diagnosticConnection = this.connectionId;
404
+ this.diagnostics = readNativeLines(this.child.stderr, async line => {
405
+ const diagnostic = agyErrorDiagnostic(line);
406
+ if (diagnostic) await this.journal("provider_error_diagnostic", { ...diagnostic, connection_id: diagnosticConnection });
407
+ else if (!/^AGY_ERROR\s*[: ]/.test(line)) await this.journal("stderr", { text: line });
408
+ }, observationFailure);
404
409
  this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
405
410
  // A dead leader is not yet a finalized tree: stop must also join the
406
411
  // pending drain, process-group check and final persistence/lease writes.
@@ -491,7 +496,11 @@ export class Runtime {
491
496
  const reducer = createAgyTranscriptFacts(source, child.provider_session_id);
492
497
  return { add: reducer.add, finish: () => {
493
498
  const facts = reducer.finish();
494
- for (const resource of facts.resources) this.nativeResources.set(resource.resource_id, mergeAgyResource(this.nativeResources.get(resource.resource_id), resource));
499
+ for (const resource of facts.resources) {
500
+ const prior = this.nativeResources.get(resource.resource_id);
501
+ this.nativeResources.set(resource.resource_id, { ...mergeAgyResource(prior, resource),
502
+ owner_daemon_id: prior?.owner_daemon_id ?? this.config.daemonId });
503
+ }
495
504
  if (facts.gap) { this.state.native_topology_error = facts.gap; this.invalidateSettlement(root); }
496
505
  for (const discovered of facts.children) {
497
506
  const expected = path.join(this.paths.runtime, 'brain', discovered.provider_session_id, '.system_generated', 'logs', 'transcript.jsonl');
@@ -1338,11 +1347,15 @@ async function dispatch(runtime, request) {
1338
1347
  }
1339
1348
  catch (error) { await runtime.recordUnconfirmedHookFailure(error, params.event, params.payload ?? {}, current, request.id); throw error; }
1340
1349
  }
1341
- if (request.operation === "daemon.stop") return await completeDaemonShutdown(runtime.paths.dir, runtime.state, patch => runtime.persist(patch), () => runtime.prepareStop(params.cancelTree === true), [["provider", () => runtime.close(false)], ["daemon_resource", () => runtime.finishResource()]], async () => {
1350
+ if (request.operation === "daemon.stop") return await completeDaemonShutdown(runtime.paths.dir, runtime.state, patch => runtime.persist(patch), () => runtime.prepareStop(params.cancelTree === true), [["provider", async ({ physicalOnly }) => {
1351
+ if (!physicalOnly) return await runtime.close(false);
1352
+ await stopProcessGroup(runtime.child);
1353
+ await finishDaemonProcess(runtime.config, runtime.providerProcess);
1354
+ }], ["daemon_resource", ({ physicalOnly }) => physicalOnly ? finishDaemonProcess(runtime.config, runtime.state.resource_process) : runtime.finishResource()]], async () => {
1342
1355
  await runtime.retireNativeServices();
1343
1356
  await runtime.captureChildTools();
1344
1357
  return runtime.receipt().native_resources;
1345
- });
1358
+ }, { cancelTree: params.cancelTree === true });
1346
1359
  throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
1347
1360
  }
1348
1361
  import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
@@ -15,6 +15,23 @@ export class AgyError extends Error {
15
15
  constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; }
16
16
  }
17
17
 
18
+ // Stderr has no operation/generation identity. Keep it diagnostic-only; never
19
+ // feed these fields into classification or continuation authority.
20
+ export function agyErrorDiagnostic(line) {
21
+ if (!/^AGY_ERROR\s*[: ]/.test(line)) return null;
22
+ try {
23
+ const envelope = JSON.parse(line.replace(/^AGY_ERROR\s*:?\s*/, ""));
24
+ const native = envelope.error ?? envelope;
25
+ const fields = {};
26
+ for (const key of ["status", "error_code", "code_kind", "retryable", "error_id"]) {
27
+ const value = native?.[key] ?? envelope[key];
28
+ if (typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)
29
+ || typeof value === "string" && /^[\w.:-]{1,128}$/.test(value)) fields[key] = value;
30
+ }
31
+ return Object.keys(fields).length ? { binding: "unbound", ...fields } : null;
32
+ } catch { return null; }
33
+ }
34
+
18
35
  export function agyTerminalFailure(result, metadata = {}) {
19
36
  let native = typeof result.error === "object" && result.error ? result.error : { message: result.error ?? "Antigravity returned ERROR" };
20
37
  if (typeof result.error === "string" && /^AGY_ERROR\s*[: ]/.test(result.error)) {
@@ -33,8 +50,10 @@ export function agyTerminalFailure(result, metadata = {}) {
33
50
  const stream = streamRequestKey(metadata.request_proof);
34
51
  const proofMissing = result.status === "ERROR" && ["provider_capacity_transient", "provider_rate_limited", "provider_network_transient"].includes(category) && !turn && !stream;
35
52
  if (proofMissing) code = "provider_overload_evidence_unavailable";
53
+ const diagnostic = agyErrorDiagnostic(`AGY_ERROR: ${JSON.stringify(native)}`);
36
54
  return new AgyError(code, String(native.message ?? "Antigravity returned ERROR"), ["provider_capacity_transient", "provider_rate_limited", "provider_network_transient"].includes(code), {
37
55
  provider_result: result, ...metadata, provider_error: safeProviderError(native), native_source: "agy.result", terminal_status: result.status === "ERROR" ? "failed" : "cancelled",
56
+ ...(diagnostic ? { native_error_diagnostic: { ...diagnostic, binding: "native_result" } } : {}),
38
57
  ...(turn ? { turn_id: turn, native_turn_id: turn } : {}),
39
58
  ...(proofMissing ? { provider_category: category, reason: "native_turn_identity_unavailable" } : {}),
40
59
  will_retry: native.willRetry === true, native_retry_active: native.willRetry === true
@@ -200,7 +200,7 @@ export async function serveDaemon(stateDir) {
200
200
  ["provider", () => bridge.close()],
201
201
  ["provider_resource", () => finishDaemonProcess(state.config, state.provider_process)],
202
202
  ["daemon_resource", () => finishDaemonProcess(state.config, state.resource_process)]
203
- ]);
203
+ ], undefined, { cancelTree: params.cancelTree === true });
204
204
  }
205
205
  if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(operation)) assertDaemonDispatch(paths.dir);
206
206
  if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
@@ -98,7 +98,7 @@ export async function serveDaemon(stateDir) {
98
98
  }],
99
99
  ["auth", () => removeDroidAuth(state.config)],
100
100
  ["daemon_resource", () => finishDaemonProcess(state.config, state.resource_process)]
101
- ]);
101
+ ], undefined, { cancelTree: params.cancelTree === true });
102
102
  }
103
103
  catch (error) { if (error.code === "tree_not_settled" && !runtime.closed) stopping = false; throw error; }
104
104
  }
@@ -541,7 +541,7 @@ export class Runtime {
541
541
  const assertDispatch = captureDispatchGuard(this);
542
542
  if (["session.create", "session.prompt", "session.fork", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
543
543
  if (operation === "daemon.ready") return { ready: this.ready, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config };
544
- if (operation === "daemon.status") { let observationError; if (!this.active && !this.state.shutdown?.phases?.tree) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { ...await this.dispatch("daemon.ready", {}), observation_error: observationError ?? this.state.observation_error ?? null }; }
544
+ if (operation === "daemon.status") { let observationError; if (!this.active && !this.state.shutdown?.phases?.execution && !this.state.shutdown?.physical_only) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { ...await this.dispatch("daemon.ready", {}), observation_error: observationError ?? this.state.observation_error ?? null }; }
545
545
  if (operation === "hook.resolve") { const sessionId = String(params.sessionId ?? ""); if (!sessionId) throw new DaemonError("hook_identity_missing", "hook has no sessionId"); let session = this.sessions.get(sessionId); if ((!session || session.parent_provider_session_id) && !this.childCreators.has(sessionId) && this.promptCreators.size) { await this.waitForChildCreator(sessionId, params.hookDeadlineAt); session = this.sessions.get(sessionId); } if (!session) { const roots = [...this.sessions.values()].filter((item) => !item.parent_provider_session_id); if (roots.length !== 1) throw new DaemonError("hook_identity_unknown", "unknown hook Session has no unique root"); const observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId, liveSession: true })); this.track(observed); session = this.sessions.get(sessionId); if (!session || session.root_provider_session_id !== roots[0].provider_session_id) throw new DaemonError("hook_identity_unknown", "hook Session is not a member of the retained native tree"); await this.persist(); } return await this.acceptHook({ daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} }, sessionId, params.payload); }
546
546
  if (operation === "hook.verify") {
547
547
  if (params.daemonId !== this.state.daemon_id) throw new DaemonError("native_hook_unproven", "Native hook daemon identity differs");
@@ -594,7 +594,7 @@ export class Runtime {
594
594
  ["provider_resource", () => finishDaemonProcess(this.state.config, this.state.provider_process)],
595
595
  ["auth", () => unlink(path.join(this.paths.home, "auth.json")).catch(error => { if (error.code !== "ENOENT") throw error; })],
596
596
  ["daemon_resource", () => finishDaemonProcess(this.state.config, this.state.resource_process)]
597
- ]);
597
+ ], undefined, { cancelTree: params.cancelTree === true });
598
598
  }
599
599
  throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
600
600
  }
@@ -668,7 +668,7 @@ export class Runtime {
668
668
  ["provider", () => stopProcessGroup(this.child)],
669
669
  ["provider_resource", () => finishDaemonProcess(this.state.config, this.providerProcess)],
670
670
  ["daemon_resource", () => finishDaemonProcess(this.state.config, this.state.resource_process)]
671
- ]);
671
+ ], undefined, { cancelTree: params.cancelTree === true });
672
672
  }
673
673
  throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
674
674
  }
@@ -693,7 +693,7 @@ export class DaemonRuntime {
693
693
  ["provider", () => this.bridge.close?.()],
694
694
  ["provider_resource", () => finishDaemonProcess(this.state.config, this.state.provider_process)],
695
695
  ["daemon_resource", () => finishDaemonProcess(this.state.config, this.state.resource_process)]
696
- ]);
696
+ ], undefined, { cancelTree: params.cancelTree === true });
697
697
  throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
698
698
  }
699
699
 
@@ -7,6 +7,7 @@ export const lifecycleRetryInstruction: string;
7
7
  export const lifecycleCommandCompletionInstruction: string;
8
8
  export const stageLifecycleInstruction: string;
9
9
  export const acceptedResultInstruction: string;
10
+ export const workStopConditionInstruction: string;
10
11
  export function workerDelegationTask(input: { workId: string; startCommand: string; launchPolicy?: string }): string;
11
12
  export function renderDelegationInstructions(input: DelegationInput): string;
12
13
  export function renderCapacityInstructions(input: { harness: string; maximum: number }): string;
@@ -1,3 +1,6 @@
1
+ // Shared by PLAN, review, CODE and leaf packets; baseline mismatch is work, not a veto.
2
+ export const workStopConditionInstruction = "Current implementation is the starting state, not an additional accepted constraint: fix mismatches with the accepted goal instead of stopping merely because they exist. Necessary project-local changes in another layer are coordination drift, not new scope; preserve ownership and active-writer fences. stop_conditions may be empty and must protect a specific accepted decision, prohibited action or unavailable resource/evidence, never invent a new user prohibition. If an actual accepted-decision conflict or required proof remains unresolved, retain its exact affected action/criterion and evidence for the coordinator; do not hide it or claim completion. An unsupported browser capability limits only its exact recorded operation/environment, not all UI work; continue independent work without substituting semantic selection for keyboard evidence.";
3
+
1
4
  const ADAPTERS = {
2
5
  codex: {
3
6
  name: "Codex",
@@ -20,6 +20,6 @@
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
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"}}}
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": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Optional action-specific accepted-decision, safety or evidence boundaries. An empty list is valid; a mismatch with current implementation or planned paths alone 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
  }
@@ -28,7 +28,7 @@
28
28
  "decision": {"type": "object", "additionalProperties": false, "required": ["id", "decision", "rationale", "affected_surfaces"], "properties": {"id": {"type": "string", "minLength": 1}, "decision": {"type": "string", "minLength": 1}, "rationale": {"type": "string", "minLength": 1}, "affected_surfaces": {"$ref": "#/$defs/strings"}, "durable_ref": {"type": "string", "minLength": 1}}},
29
29
  "documentUpdate": {"type": "object", "additionalProperties": false, "required": ["path", "action", "owner", "reason"], "properties": {"path": {"type": "string", "minLength": 1}, "action": {"enum": ["create", "update"]}, "owner": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}}},
30
30
  "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": {"type": "array", "items": {"type": "string", "minLength": 1}}, "acceptance_contribution": {"type": "string", "minLength": 1}}},
31
- "execution": {"type": "object", "additionalProperties": false, "required": ["required_read", "discovery_boundary", "planned_write_areas", "stop_conditions"], "properties": {"required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources. This is not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas. A worker may inspect other project-local sources when necessary."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Optional file or directory hints used only to coordinate concurrent Works. They never grant or deny write permission."}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Semantic contradictions or external blockers that require the worker to stop."}}},
31
+ "execution": {"type": "object", "additionalProperties": false, "required": ["required_read", "discovery_boundary", "planned_write_areas", "stop_conditions"], "properties": {"required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources. This is not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas. A worker may inspect other project-local sources when necessary."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Optional file or directory hints used only to coordinate concurrent Works. They never grant or deny write permission."}, "stop_conditions": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Optional action-specific accepted-decision, safety or evidence boundaries. An empty list is valid; a mismatch with current implementation or planned paths alone is not a stop condition."}}},
32
32
  "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", "pattern": "^P[0-9]+$"}, "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/"}}}}, {"if": {"properties": {"command": {"pattern": "^@check/"}}}, "then": {"required": ["definition"]}}]},
33
33
  "verification": {"type": "object", "additionalProperties": false, "required": ["check_refs"], "properties": {"check_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}}}},
34
34
  "item": {"type": "object", "additionalProperties": false, "required": ["id", "title", "summary", "details", "depends_on", "requirement_refs", "semantic_spine", "execution_context", "verification"], "properties": {"id": {"type": "string", "pattern": "^P[0-9]+$"}, "title": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}, "details": {"type": "string", "minLength": 1}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^P[0-9]+$"}}, "requirement_refs": {"$ref": "#/$defs/strings"}, "semantic_spine": {"$ref": "#/$defs/spine"}, "execution_context": {"$ref": "#/$defs/execution"}, "verification": {"$ref": "#/$defs/verification"}}},