@deksden-com/dd-flow-cli 0.8.0-beta.135 → 0.8.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Patch Changes
6
+
7
+ - a092c98: Accept controlled OpenCode lifecycle events and cumulative root/child Session usage through the shared harness evidence path.
8
+ - fb2395a: Accept trusted lifecycle events from controlled zcode-acp sessions and retain harness-native root, child, parent, and execution-daemon identities in Session evidence.
9
+ - Promote the accepted SPECIFY-through-CODE-REVIEW runtime as stable 0.8.0 and align its shared compatibility, flow, stage-start and status schemas with Memory Bank 4.0.0.
10
+
11
+ ## 0.8.0-beta.138
12
+
13
+ ### Patch Changes
14
+
15
+ - Replace CODE and repair file allowlists with optional `planned_write_areas`:
16
+ soft coordination hints that serialize likely concurrent collisions but never
17
+ deny a necessary project-local edit. Record unexpected actual paths as
18
+ `coordination.drift` instead of rejecting the Work.
19
+ - Render the hard RUN workspace boundary separately from required reads,
20
+ discovery hints and planned coordination areas, so a worker can distinguish
21
+ semantic constraints from planning predictions.
22
+
23
+ ## 0.8.0-beta.137
24
+
25
+ ### Patch Changes
26
+
27
+ - Transfer a running coordinator Work to a fresh Session at a frozen
28
+ `new_session` stage boundary, while still rejecting a second Session that
29
+ tries to repeat the same stage.
30
+
31
+ ## 0.8.0-beta.136
32
+
33
+ ### Patch Changes
34
+
35
+ - Serialize the long CODE aggregate gate and reuse complete same-workspace
36
+ receipts after a client loses its command output, preventing a concurrent
37
+ retry from duplicating checks or colliding on receipt identifiers.
38
+
3
39
  ## 0.8.0-beta.135
4
40
 
5
41
  ### Patch Changes
package/README.md CHANGED
@@ -97,7 +97,7 @@ dd-flow prompt render \
97
97
 
98
98
  For a non-plan worker, replace `--plan-item` with `--task-file <path>` pointing to a `dd-flow/worker-task@1` manifest inside the selected RUN home. The manifest embeds one existing plan-item-shaped `task`, so validation and rendered artifacts remain identical to the compatibility route.
99
99
 
100
- The selected item must contain `execution_context` and `semantic_spine`. The renderer accepts only registered profiles, validates the active RUN/workspace and source paths, then persists `launch-prompt.md`, `prompt-stack.json` and `render-report.json` in the RUN stage. The stack and render report both retain the validated read/discovery/write references. It records project sources as paths rather than copying their contents. `required_read` and `discovery_boundary` may use a checked `run://<relative-path>` reference for a source inside the selected RUN home; write scopes remain project-local.
100
+ The selected item must contain `execution_context` and `semantic_spine`. The renderer accepts only registered profiles, validates the active RUN/workspace and source paths, then persists `launch-prompt.md`, `prompt-stack.json` and `render-report.json` in the RUN stage. The stack and render report both retain the validated read/discovery/coordination references. It records project sources as paths rather than copying their contents. `required_read` and `discovery_boundary` may use a checked `run://<relative-path>` reference for a source inside the selected RUN home. The RUN workspace is the hard write boundary; `planned_write_areas` are optional soft hints for concurrent-work coordination, never a file allowlist.
101
101
 
102
102
  Human help is available for the operator-facing command groups:
103
103
 
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.8.0-beta.135",
4
- "cli_commit": "05b606d42418d944364abb013ce02f3e78149916",
5
- "built_at": "2026-08-30T15:27:46.996Z",
3
+ "cli_version": "0.8.0",
4
+ "cli_commit": "f0a5e35190c960c16caa0913837d4426b3c35db7",
5
+ "built_at": "2026-08-31T10:23:34.510Z",
6
6
  "built_with_canon": {
7
- "version": null,
8
- "commit": null,
9
- "flow_contract": null,
10
- "repo_root": null,
11
- "memorybank_root": null,
12
- "flow_root": null,
13
- "layout": null
7
+ "version": "4.0.0",
8
+ "commit": "39728d7091691ad32e94d7b79b3656821c63ccc2",
9
+ "flow_contract": "dd-flow-canonical-2026-08",
10
+ "repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
11
+ "memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
12
+ "flow_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank/dd-flow",
13
+ "layout": "dot_memory_bank"
14
14
  }
15
15
  }
package/dist/cli/help.js CHANGED
@@ -204,9 +204,9 @@ Usage:
204
204
  dd-flow work show <WORK-ID> --json
205
205
  dd-flow work start <WORK-ID> --project-root <root> --json
206
206
  dd-flow work finish <WORK-ID> (--result-file <path>|--result-stdin) --project-root <root> --json
207
- dd-flow work repair add --run <RUN-ID> --from-check <CHECK-ID> --origin-work <WORK-ID>... [--write-scope <path>...] --task-stdin --project-root <root> --json
207
+ dd-flow work repair add --run <RUN-ID> --from-check <CHECK-ID> --origin-work <WORK-ID>... --task-stdin --project-root <root> --json
208
208
 
209
- CODE workers receive their full accepted packet from work start. work finish validates the result and executes the packet's focused checks before completion. A failed aggregate CODE gate returns the exact failed receipt and repair command; repair add creates a new Work from the selected completed origin context plus that failure evidence. Exact existing project files named by the retained receipt output are added to its repair scope deterministically; repeat --write-scope only for an additional path not named there.`
209
+ CODE workers receive their full accepted packet from work start. work finish validates the result and executes the packet's focused checks before completion. A failed aggregate CODE gate returns the exact failed receipt and repair command; repair add creates a new Work from the selected completed origin context plus that failure evidence. Its planned write areas are coordination hints only; the repair may change any necessary project file inside its RUN workspace.`
210
210
  ],
211
211
  [
212
212
  "id",
@@ -666,7 +666,6 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
666
666
  projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
667
667
  runId: requiredOption(parsed, "run"),
668
668
  findingIds: findings,
669
- writeScope: parsed.options.get("write-scope") ?? [],
670
669
  objective: await readStdin(io.stdin)
671
670
  });
672
671
  if (!hasOption(parsed, "from-check"))
@@ -678,7 +677,6 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
678
677
  runId: requiredOption(parsed, "run"),
679
678
  checkReceiptId: requiredOption(parsed, "from-check"),
680
679
  originWorkIds: origins,
681
- writeScope: parsed.options.get("write-scope") ?? [],
682
680
  objective: await readStdin(io.stdin)
683
681
  });
684
682
  }
@@ -113,7 +113,7 @@ function validateExecutionContext(input, itemId) {
113
113
  prompt_profile: requireString(context.prompt_profile, `Plan item ${itemId} execution_context.prompt_profile`),
114
114
  required_read: requireStringArray(context.required_read, `Plan item ${itemId} execution_context.required_read`),
115
115
  discovery_boundary: requireStringArray(context.discovery_boundary, `Plan item ${itemId} execution_context.discovery_boundary`),
116
- write_scope: requireStringArray(context.write_scope, `Plan item ${itemId} execution_context.write_scope`),
116
+ planned_write_areas: requireStringArray(context.planned_write_areas, `Plan item ${itemId} execution_context.planned_write_areas`),
117
117
  checks: requireStringArray(context.checks, `Plan item ${itemId} execution_context.checks`)
118
118
  };
119
119
  }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "dd-flow/code-work-batch@4",
3
+ "$id": "dd-flow/code-work-batch@5",
4
4
  "type": "object",
5
5
  "additionalProperties": false,
6
6
  "required": ["schema_id", "sources", "works"],
7
7
  "properties": {
8
- "schema_id": {"const": "dd-flow/code-work-batch@4"},
8
+ "schema_id": {"const": "dd-flow/code-work-batch@5"},
9
9
  "sources": {"type": "array", "minItems": 1, "items": {"type": "object", "additionalProperties": false, "required": ["plan_id", "protocol_id", "revision", "sha256"], "properties": {"plan_id": {"type": "string", "minLength": 1}, "protocol_id": {"type": "string", "minLength": 1}, "revision": {"type": "integer", "minimum": 1}, "sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}}}},
10
10
  "works": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/work"}}
11
11
  },
@@ -19,6 +19,6 @@
19
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}}}, "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
  "repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_finding_ids", "review_evidence_refs"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_finding_ids": {"$ref": "#/$defs/strings"}, "review_evidence_refs": {"$ref": "#/$defs/strings"}}},
22
- "work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "write_scope", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@4"}, "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"}, "discovery_boundary": {"$ref": "#/$defs/strings"}, "write_scope": {"$ref": "#/$defs/strings"}, "checks": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings"}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@2"}}}
22
+ "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", "minItems": 1, "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"}}}
23
23
  }
24
24
  }
@@ -51,6 +51,38 @@
51
51
  "status": { "type": "string", "minLength": 1 }
52
52
  }
53
53
  },
54
+ "upgrade_contract": {
55
+ "type": "object",
56
+ "additionalProperties": false,
57
+ "required": ["model", "authorization", "storage_mutation", "run_creation", "post_sync_routing", "contexts", "order"],
58
+ "properties": {
59
+ "model": { "const": "dual_context_target_routing" },
60
+ "authorization": { "const": "command_allowlist" },
61
+ "storage_mutation": { "const": "explicit_migration_apply_only" },
62
+ "run_creation": { "const": "after_storage_readiness" },
63
+ "post_sync_routing": { "const": "marker_free_project_current" },
64
+ "contexts": {
65
+ "type": "array",
66
+ "const": ["project_current", "upgrade_target", "effective_execution"]
67
+ },
68
+ "order": {
69
+ "type": "array",
70
+ "const": [
71
+ "pin_target_canon",
72
+ "verify_target_router",
73
+ "resolve_contexts",
74
+ "read_only_diagnostics",
75
+ "check_shared_home_activity",
76
+ "verify_backup",
77
+ "plan_adjacent_migration",
78
+ "apply_migration_explicitly",
79
+ "create_upgrade_run",
80
+ "sync_canonical_layer",
81
+ "verify_marker_free_normal_route"
82
+ ]
83
+ }
84
+ }
85
+ },
54
86
  "contracts": {
55
87
  "type": "object",
56
88
  "additionalProperties": false,
@@ -34,12 +34,13 @@
34
34
  "specifications": {
35
35
  "type": "array",
36
36
  "minItems": 3,
37
+ "uniqueItems": true,
37
38
  "items": {
38
39
  "type": "object",
39
40
  "additionalProperties": false,
40
41
  "required": ["id", "version", "sha256"],
41
42
  "properties": {
42
- "id": {"enum": ["SPC-004", "SPC-005", "SPC-006"]},
43
+ "id": {"enum": ["SPC-004", "SPC-005", "SPC-006", "SPC-009", "SPC-010", "SPC-011"]},
43
44
  "version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"},
44
45
  "sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}
45
46
  }
@@ -180,7 +181,7 @@
180
181
  "source_of_truth": {"const": "canonical prompts and schemas"},
181
182
  "runtime_owner": {"const": "dd-flow-cli"},
182
183
  "lint_owner": {"const": "mb-lint"},
183
- "integrated_gate": {"const": "compatible releases plus clean specify-to-plan eval"}
184
+ "integrated_gate": {"type": "string", "minLength": 1}
184
185
  }
185
186
  }
186
187
  }
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "definitions": {
23
23
  "semanticSpine": {"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": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "non_goals": {"type": "array", "items": {"type": "string", "minLength": 1}}, "acceptance_contribution": {"type": "string", "minLength": 1}}},
24
- "executionContext": {"type": "object", "additionalProperties": false, "required": ["prompt_profile", "required_read", "discovery_boundary", "write_scope", "checks"], "properties": {"prompt_profile": {"enum": ["documentation_contract", "code_implementation", "verification"]}, "required_read": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "discovery_boundary": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "write_scope": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "checks": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}},
24
+ "executionContext": {"type": "object", "additionalProperties": false, "required": ["prompt_profile", "required_read", "discovery_boundary", "planned_write_areas", "checks"], "properties": {"prompt_profile": {"enum": ["documentation_contract", "code_implementation", "verification"]}, "required_read": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "discovery_boundary": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft concurrent-work coordination hints, never a write allowlist."}, "checks": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}},
25
25
  "verificationContract": {"type": "object", "additionalProperties": false, "required": ["checks", "evidence"], "properties": {"checks": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "evidence": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}},
26
26
  "planItem": {"type": "object", "additionalProperties": false, "required": ["id", "kind", "title", "summary", "depends_on", "owner", "target_stage", "required", "requirements", "semantic_spine", "execution_context", "verification_contract"], "properties": {"id": {"type": "string", "pattern": "^P[0-9]+$"}, "kind": {"type": "string", "minLength": 1}, "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]+$"}}, "owner": {"type": "string", "minLength": 1}, "target_stage": {"enum": ["specify", "plan", "code", "readiness", "merge", "release"]}, "required": {"type": "boolean"}, "requirements": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "pattern": "^SPC-[0-9]{3}@[0-9]+\\.[0-9]+\\.[0-9]+/R-[0-9]+$"}}, "semantic_spine": {"$ref": "#/definitions/semanticSpine"}, "execution_context": {"$ref": "#/definitions/executionContext"}, "verification_contract": {"$ref": "#/definitions/verificationContract"}}},
27
27
  "taskAssessment": {"type": "object", "additionalProperties": false, "required": ["scope_breadth", "solution_novelty", "solution_uncertainty", "failure_impact", "plan_floor"], "properties": {"scope_breadth": {"$ref": "#/definitions/assessmentAxis"}, "solution_novelty": {"$ref": "#/definitions/assessmentAxis"}, "solution_uncertainty": {"$ref": "#/definitions/assessmentAxis"}, "failure_impact": {"$ref": "#/definitions/assessmentAxis"}, "plan_floor": {"$ref": "#/definitions/assessmentAxis"}}},
@@ -10,7 +10,7 @@
10
10
  "then": {"required": ["plan_ref", "aspect_map_ref"]}
11
11
  }
12
12
  ],
13
- "required": ["schema_id", "run_id", "stage", "attempt_number", "stage_root", "prompt_path", "aliases", "next_command", "worker_prompt_markdown"],
13
+ "required": ["schema_id", "run_id", "stage", "attempt_number", "stage_root", "prompt_path", "aliases", "resolved_context", "preflight", "worker_prompt_markdown", "next_command"],
14
14
  "properties": {
15
15
  "schema_id": {"const": "dd-flow/stage-start-response@2"},
16
16
  "run_id": {"type": "string", "minLength": 1},
@@ -24,7 +24,9 @@
24
24
  "plan_ref": {"type": "string", "pattern": "^\\.memory-bank/protocol/[^/]+/plan\\.json$"},
25
25
  "aspect_map_ref": {"type": "string", "minLength": 1},
26
26
  "next_command": {"type": "string", "pattern": "^dd-flow stage finish "},
27
+ "preflight": {"type": "object", "required": ["git", "compatibility", "permissions", "session_binding"], "additionalProperties": true},
28
+ "worker_prompt_markdown": {"type": "string", "minLength": 1},
27
29
  "permission_probe": {"type": "object", "additionalProperties": true},
28
- "worker_prompt_markdown": {"type": "string", "minLength": 1}
30
+ "bootstrap": {"type": "boolean"}
29
31
  }
30
32
  }
@@ -84,9 +84,85 @@
84
84
  }
85
85
  }
86
86
  }
87
+ },
88
+ "engine": {
89
+ "type": ["object", "null"],
90
+ "additionalProperties": true,
91
+ "properties": {
92
+ "contexts": { "$ref": "#/definitions/compatibilityContexts" }
93
+ }
87
94
  }
88
95
  },
89
96
  "definitions": {
97
+ "compatibilityContexts": {
98
+ "type": "object",
99
+ "additionalProperties": false,
100
+ "required": ["project_current", "upgrade_target", "effective_execution"],
101
+ "properties": {
102
+ "project_current": { "$ref": "#/definitions/projectCurrentContext" },
103
+ "upgrade_target": {
104
+ "anyOf": [
105
+ { "type": "null" },
106
+ { "$ref": "#/definitions/upgradeTargetContext" }
107
+ ]
108
+ },
109
+ "effective_execution": { "$ref": "#/definitions/effectiveExecutionContext" }
110
+ }
111
+ },
112
+ "projectCurrentContext": {
113
+ "type": "object",
114
+ "additionalProperties": true,
115
+ "required": ["name", "canon", "memory_bank_version", "package_name", "required_engine_range", "resolution", "engine_version", "diagnostics"],
116
+ "properties": {
117
+ "name": { "const": "project_current" },
118
+ "canon": { "type": "object", "additionalProperties": true },
119
+ "memory_bank_version": { "type": ["string", "null"] },
120
+ "package_name": { "type": "string", "minLength": 1 },
121
+ "required_engine_range": { "type": ["string", "null"] },
122
+ "recommended_engine_version": { "type": ["string", "null"] },
123
+ "resolution": { "type": "string", "minLength": 1 },
124
+ "engine_version": { "type": ["string", "null"] },
125
+ "diagnostics": { "type": "array", "items": { "type": "string" } }
126
+ }
127
+ },
128
+ "upgradeTargetContext": {
129
+ "type": "object",
130
+ "additionalProperties": true,
131
+ "required": ["name", "canon", "memory_bank_version", "package_name", "required_engine_range", "resolution", "engine_version", "diagnostics"],
132
+ "properties": {
133
+ "name": { "const": "upgrade_target" },
134
+ "canon": {
135
+ "type": "object",
136
+ "additionalProperties": true,
137
+ "required": ["root", "version", "commit", "compatibility_sha256"],
138
+ "properties": {
139
+ "root": { "type": "string", "minLength": 1 },
140
+ "version": { "type": "string", "minLength": 1 },
141
+ "commit": { "type": "string", "minLength": 1 },
142
+ "compatibility_sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
143
+ }
144
+ },
145
+ "memory_bank_version": { "type": ["string", "null"] },
146
+ "package_name": { "type": "string", "minLength": 1 },
147
+ "required_engine_range": { "type": ["string", "null"] },
148
+ "recommended_engine_version": { "type": ["string", "null"] },
149
+ "resolution": { "type": "string", "minLength": 1 },
150
+ "engine_version": { "type": ["string", "null"] },
151
+ "diagnostics": { "type": "array", "items": { "type": "string" } }
152
+ }
153
+ },
154
+ "effectiveExecutionContext": {
155
+ "type": "object",
156
+ "additionalProperties": false,
157
+ "required": ["operation", "mode", "package_name", "engine_version", "reason"],
158
+ "properties": {
159
+ "operation": { "type": "string", "minLength": 1 },
160
+ "mode": { "type": "string", "minLength": 1 },
161
+ "package_name": { "type": "string", "minLength": 1 },
162
+ "engine_version": { "type": ["string", "null"] },
163
+ "reason": { "type": "string", "minLength": 1 }
164
+ }
165
+ },
90
166
  "cliCompatibility": {
91
167
  "type": "object",
92
168
  "additionalProperties": true,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "dd-flow/protocol-plan@5",
3
+ "$id": "dd-flow/protocol-plan@6",
4
4
  "title": "Semantic protocol implementation plan",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
7
  "required": ["schema_id", "plan_id", "protocol_id", "revision", "title", "summary", "source_refs", "goal", "assessment", "decisions", "document_updates", "checks", "items", "acceptance"],
8
8
  "properties": {
9
- "schema_id": {"const": "dd-flow/protocol-plan@5"},
9
+ "schema_id": {"const": "dd-flow/protocol-plan@6"},
10
10
  "plan_id": {"type": "string", "pattern": "^PLAN-[A-Za-z0-9][A-Za-z0-9._-]*$"},
11
11
  "protocol_id": {"type": "string", "pattern": "^PRT-[A-Za-z0-9][A-Za-z0-9._-]*$"},
12
12
  "revision": {"type": "integer", "minimum": 1},
@@ -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", "write_scope", "stop_conditions"], "properties": {"required_read": {"$ref": "#/$defs/strings"}, "discovery_boundary": {"$ref": "#/$defs/strings"}, "write_scope": {"$ref": "#/$defs/strings"}, "stop_conditions": {"$ref": "#/$defs/strings"}}},
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."}}},
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", "external"]}, "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}}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}]},
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"}}},
@@ -127,7 +127,7 @@ export function aggregateCheckDeclarations(workspaceRoot) {
127
127
  /**
128
128
  * A project-wide gate can observe files owned by more than one CODE Work.
129
129
  * Keep it at the CODE/readiness fan-in, where the engine can create a repair
130
- * Work with the failed receipt and the right write scope.
130
+ * Work with the failed receipt and the right project-local repair context.
131
131
  */
132
132
  export function validateCheckPlacement(workspaceRoot, checks) {
133
133
  const aggregate = new Set(aggregateCheckDeclarations(workspaceRoot).map((check) => check.command));
@@ -69,12 +69,12 @@ export function renderWorkerPrompt(context, input) {
69
69
  runId: run.id,
70
70
  protocolId: protocol.id,
71
71
  planItemId: item.id,
72
- groupId: item.execution_context.write_scope[0] ?? null
72
+ groupId: item.execution_context.planned_write_areas[0] ?? null
73
73
  });
74
74
  const staticInputs = profile.static_files.map((file) => readStaticInput(projectRoot, file));
75
75
  const requiredRead = item.execution_context.required_read.map((file) => checkedReference(projectRoot, runHomePath(run), file, "required_read", true));
76
76
  const discoveryBoundary = item.execution_context.discovery_boundary.map((file) => checkedReference(projectRoot, runHomePath(run), file, "discovery_boundary", false));
77
- const writeScope = item.execution_context.write_scope.map((file) => checkedReference(projectRoot, undefined, file, "write_scope", false));
77
+ const plannedWriteAreas = item.execution_context.planned_write_areas.map((file) => checkedReference(projectRoot, undefined, file, "planned_write_areas", false));
78
78
  const runHome = runHomePath(run);
79
79
  const outputDir = path.join(runHome, stage.dir, "subagents", item.id);
80
80
  assertWithin(runHome, outputDir, "output directory");
@@ -85,7 +85,7 @@ export function renderWorkerPrompt(context, input) {
85
85
  workspaceRoot,
86
86
  requiredRead,
87
87
  discoveryBoundary,
88
- writeScope,
88
+ plannedWriteAreas,
89
89
  outputDir,
90
90
  staticInputs,
91
91
  ...(genericTask ? { handoff: genericTask.handoff } : {})
@@ -107,7 +107,7 @@ export function renderWorkerPrompt(context, input) {
107
107
  canon_version: readCanonVersion(projectRoot),
108
108
  renderer_version: getCliVersionReport().cli.version,
109
109
  static_inputs: staticInputs.map(({ path: inputPath, sha256 }) => ({ path: inputPath, sha256 })),
110
- validation: { required_read: requiredRead, discovery_boundary: discoveryBoundary, write_scope: writeScope },
110
+ validation: { required_read: requiredRead, discovery_boundary: discoveryBoundary, planned_write_areas: plannedWriteAreas },
111
111
  ...(genericTask ? { task_handoff: genericTask.handoff } : {})
112
112
  });
113
113
  writeJson(reportPath, {
@@ -121,7 +121,7 @@ export function renderWorkerPrompt(context, input) {
121
121
  stage: input.stage,
122
122
  workspace_root: workspaceRoot,
123
123
  output: { launch_prompt: promptPath, prompt_stack: stackPath },
124
- validation: { required_read: requiredRead, discovery_boundary: discoveryBoundary, write_scope: writeScope },
124
+ validation: { required_read: requiredRead, discovery_boundary: discoveryBoundary, planned_write_areas: plannedWriteAreas },
125
125
  ...(genericTask ? { task_handoff: genericTask.handoff } : {})
126
126
  });
127
127
  return {
@@ -290,9 +290,10 @@ function renderPrompt(input) {
290
290
  `- Non-goals: ${(item.semantic_spine?.non_goals ?? []).join("; ") || "none"}`,
291
291
  `- Acceptance contribution: ${item.semantic_spine?.acceptance_contribution ?? "not_applicable"}`,
292
292
  "",
293
- "## Runtime",
294
- `- Workspace: \`${input.workspaceRoot}\``,
295
- `- Allowed writes: ${input.writeScope.map((value) => `\`${value}\``).join(", ") || "none"}`,
293
+ "## Hard Write Boundary",
294
+ `- All project writes must stay under: \`${input.workspaceRoot}\``,
295
+ "- Do not write outside this workspace, to another RUN, or into Git/worktree control data.",
296
+ "- Accepted requirements and non-goals remain binding; planned write areas do not.",
296
297
  `- Checks: ${(item.execution_context?.checks ?? []).map((value) => `\`${value}\``).join(", ") || "none"}`,
297
298
  `- Report directory: \`${input.outputDir}\``,
298
299
  "",
@@ -307,10 +308,15 @@ function renderPrompt(input) {
307
308
  ""
308
309
  ]
309
310
  : []),
310
- "## Bounded Discovery",
311
+ "## Discovery Hints",
311
312
  ...input.discoveryBoundary.map((value) => `- \`${value}\``),
312
313
  "",
313
- "Read the static instructions below before acting. Report the sources actually read and any additions inside the discovery boundary. If an essential source is outside that boundary, stop with a plan question or DEF rather than expanding scope silently.",
314
+ "## Planned Write Areas",
315
+ "These are SOFT coordination hints for parallel workers, not an allowlist or permission boundary.",
316
+ `- Areas: ${input.plannedWriteAreas.map((value) => `\`${value}\``).join(", ") || "none predicted"}`,
317
+ "- You may read or change any project-local file needed to complete the accepted task. Report every actual changed path.",
318
+ "",
319
+ "Read the static instructions below before acting. Read every required source first; discovery hints do not prohibit necessary additional project-local reads. Report material additional sources and changes.",
314
320
  "",
315
321
  ...staticInputs.flatMap((entry) => [`## Static Input: ${entry.path}`, "", entry.content.trim(), ""])
316
322
  ].join("\n");
@@ -90,7 +90,6 @@ export function addVnextCodeReviewRepair(context, input) {
90
90
  reviewFindingIds: selected.map((finding) => finding.finding_ref),
91
91
  reviewEvidenceRefs: selected.flatMap(({ finding }) => finding.evidence_refs),
92
92
  originWorkIds: origins,
93
- writeScope: input.writeScope,
94
93
  objective: input.objective
95
94
  });
96
95
  }
@@ -129,8 +128,7 @@ export async function finishVnextCodeReview(context, input) {
129
128
  const fixIds = canonical.fix_ids;
130
129
  if (fixIds.length && repairs.length === 0) {
131
130
  fs.writeFileSync(frozenShaFile, `${decisionSha}\n`);
132
- const writeScope = uniqueStrings(codeWorks(context, project.id, run.id).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.write_scope) ? payload.write_scope.filter((value) => typeof value === "string") : []; }));
133
- const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, writeScope, objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
131
+ const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
134
132
  return { ok: true, run_id: run.id, stage, outcome: "repair_required", decision_sha256: decisionSha, repair, instruction: "Run the returned repair Work in one fresh child session. When it completes, invoke the same stage finish command again with the unchanged decision file.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
135
133
  }
136
134
  repairs = reviewRepairWorks(context, project.id, run.id, root);
@@ -242,7 +240,7 @@ function validateDecision(context, input) {
242
240
  function validateReviewerResult(context, work, run) { if (!work.result)
243
241
  throw new AppError("review_evidence_invalid", "Reviewer Work has no result", 2, { work_id: work.work_id }); const file = path.join(requireHome(run), stageDir, `${work.work_id}.result.json`); fs.writeFileSync(file, work.result); validateSchema({ schemaName: "code-review-result", file, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id }); }
244
242
  function reviewerWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const p = readJsonString(work.payload_json); return Boolean(p && p.kind === "code-review"); }); }
245
- function codeWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND status = 'completed' ORDER BY created_at, work_id", [projectId, runId]).filter((work) => readJsonString(work.payload_json).schema_id === "dd-flow/code-work-packet@4" && !reviewFindingIds(work).length); }
243
+ function codeWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND status = 'completed' ORDER BY created_at, work_id", [projectId, runId]).filter((work) => readJsonString(work.payload_json).schema_id === "dd-flow/code-work-packet@5" && !reviewFindingIds(work).length); }
246
244
  function reviewRepairWorks(context, projectId, runId, root) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => isCodeReviewStageRepair(work, root)); }
247
245
  export function isCodeReviewStageRepair(work, root) {
248
246
  if (reviewFindingIds(work).length)
@@ -267,7 +265,6 @@ catch {
267
265
  return [];
268
266
  } }
269
267
  function sha256File(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
270
- function uniqueStrings(values) { return [...new Set(values)]; }
271
268
  function stageStatus(run, name) { try {
272
269
  return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === name)?.status ?? null;
273
270
  }
@@ -4,7 +4,7 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { AppError } from "../shared/errors.js";
6
6
  import { resolveProjectRoot } from "../storage/paths.js";
7
- import { aggregateCheckDeclarations, checkReceipts, codeExecutionEnvironment, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures } from "./code-checks.js";
7
+ import { aggregateCheckDeclarations, checkReceipts, codeExecutionEnvironment, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
8
8
  import { requireProjectByRoot } from "./projects.js";
9
9
  import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
10
10
  import { validateSchema } from "./schema-validation.js";
@@ -141,15 +141,26 @@ export async function finishVnextCode(context, input) {
141
141
  const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: checks });
142
142
  if (unchangedFailures.length)
143
143
  throw new AppError("code_gate_repair_required", "CODE final gate already failed for the unchanged workspace; repair the evidenced failure before retrying", 2, { outcome: "repair_required", retry_after_workspace_change: true, workspace_fingerprint: unchangedFailures[0].workspace_fingerprint, failures: unchangedFailures });
144
- const receipts = await runCodeChecks(context, {
145
- projectId: project.id,
146
- runId: run.id,
147
- runHome: home,
148
- workspaceRoot: run.workspace_root,
149
- scope: "aggregate",
150
- checks,
151
- ...(input.progress ? { progress: input.progress } : {})
152
- });
144
+ let receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks });
145
+ if (!receipts) {
146
+ const release = acquireAggregateGateLock(root);
147
+ try {
148
+ // A prior caller may have completed the slow gate while this caller was
149
+ // waiting to acquire the lock. Reuse that immutable evidence.
150
+ receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks }) ?? await runCodeChecks(context, {
151
+ projectId: project.id,
152
+ runId: run.id,
153
+ runHome: home,
154
+ workspaceRoot: run.workspace_root,
155
+ scope: "aggregate",
156
+ checks,
157
+ ...(input.progress ? { progress: input.progress } : {})
158
+ });
159
+ }
160
+ finally {
161
+ release();
162
+ }
163
+ }
153
164
  const failed = receipts.filter((receipt) => receipt.status === "failed");
154
165
  if (failed.length) {
155
166
  throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
@@ -271,7 +282,7 @@ export function addVnextCodeRepair(context, input) {
271
282
  const key = input.reviewFindingIds?.length ? "code-review-repair" : "code-gate-repair";
272
283
  const receiptWriteScope = receipt ? receiptRepairPaths(run.workspace_root, receipt) : [];
273
284
  const repair = {
274
- schema_id: "dd-flow/code-work-packet@4",
285
+ schema_id: "dd-flow/code-work-packet@5",
275
286
  key,
276
287
  launch_policy: "fresh_agent_required",
277
288
  source: first.source,
@@ -293,11 +304,9 @@ export function addVnextCodeRepair(context, input) {
293
304
  document_updates: receipt ? [] : uniqueBy(packets.flatMap((value) => value.document_updates).filter((update) => (input.reviewEvidenceRefs ?? []).some((reference) => reference === update.path || reference.startsWith(`${update.path}:`) || reference.endsWith(`/${update.path}`))), (value) => JSON.stringify(value)),
294
305
  required_read: unique([...(receipt ? [receipt.receipt_path] : input.reviewEvidenceRefs ?? []), ...packets.flatMap((value) => value.required_read)]),
295
306
  discovery_boundary: unique(packets.flatMap((value) => value.discovery_boundary)),
296
- // A failed deterministic check can name an exact existing project file
297
- // which was not owned by its origin Work (for example a formatter's
298
- // project-level configuration). Admit only those normalized file paths;
299
- // never infer a directory or accept a path outside the workspace.
300
- write_scope: unique([...packets.flatMap((value) => value.write_scope), ...input.writeScope, ...receiptWriteScope]),
307
+ // These are collision-avoidance hints. Receipt paths enrich the coordinator
308
+ // picture but never restrict the repair's project-local edits.
309
+ planned_write_areas: unique([...packets.flatMap((value) => value.planned_write_areas), ...receiptWriteScope]),
301
310
  // Receipts record the resolved shell command. A repair must retain the
302
311
  // accepted declaration (including its immutable @check alias) and only
303
312
  // change when it runs, so work finish can validate it again.
@@ -305,7 +314,7 @@ export function addVnextCodeRepair(context, input) {
305
314
  provides_checks: [],
306
315
  stop_conditions: unique([
307
316
  ...invariantPackets.flatMap((value) => value.stop_conditions),
308
- ...(receipt ? ["Stop and report the blocker if the evidenced root cause is outside write_scope or a proposed repair would contradict an accepted requirement."] : [])
317
+ ...(receipt ? ["Stop and report the blocker if the proposed repair contradicts an accepted requirement or non-goal."] : [])
309
318
  ]),
310
319
  depends_on: origins.map((work) => work.work_id),
311
320
  result_schema: "dd-flow/code-work-result@2"
@@ -382,7 +391,7 @@ function coordinatorPrompt(context, input) {
382
391
  "",
383
392
  "<execution_commands>",
384
393
  "Launch only entries listed in graph.ready. Use at most the known available slot count. Every registered CODE Work runs in a fresh child Session, including a serial dependency chain; the coordinator owns dispatch and the stage conclusion, not implementation Work. Every child starts with its exact start_command and receives its complete packet from dd-flow. After a Work finishes, use the graph returned by work finish to launch newly ready Work. To refresh the parent graph yourself use the exact command: " + `${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`,
385
- "A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. Close a disposable child only after its Work is accepted or explicitly failed/cancelled and the harness reports the turn settled.",
394
+ "A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. After you issue the exact CODE stage finish command, wait for that same command to return: do not inspect its PID, start a second finish command, or infer failure from quiet output. Close a disposable child only after its Work is accepted or explicitly failed/cancelled and the harness reports the turn settled.",
386
395
  `A repairable engine, harness, or environment failure is not a user question. Record it without finishing CODE: ${flowCommand(context)} stage block ${input.run.id} --stage code --work ${input.rootWork.work_id} --kind <engine|harness|environment> --code <stable-code> --summary-stdin --retryable --project-root ${JSON.stringify(input.projectRoot)} --json. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
387
396
  `When every CODE and repair Work is completed, write ${path.join(input.root, "code-verification.json")} using the exact contract below. Mark passed only when all accepted requirements and current-gate acceptance criteria are implemented or explicitly evidenced; list every remaining issue in unresolved. Every evidence_refs item must already exist as a relative workspace path or run://${input.run.id}/ path. Do not claim a browser or other check receipt that was not retained. Then finish: ${finishCommand(context, input.run.id, input.projectRoot, path.join(input.root, "code-verification.json"))}`,
388
397
  "If the aggregate gate fails, use the returned repair command with the relevant completed origin Work IDs and a concise repair objective. Do not edit invisibly in the root orchestrator.",
@@ -410,6 +419,67 @@ function latestReceiptsByCommand(receipts) {
410
419
  latest.set(receipt.declaration_id, receipt);
411
420
  return [...latest.values()];
412
421
  }
422
+ /**
423
+ * A CODE finish may be retried after a client loses the long command's output.
424
+ * Reusing complete, same-workspace receipts is correct; rerunning them is both
425
+ * expensive and can race the still-settling original invocation.
426
+ */
427
+ export function reusableAggregateGateReceipts(context, input) {
428
+ const expected = new Set(input.checks.map((check) => check.id));
429
+ const current = workspaceFingerprint(input.workspaceRoot);
430
+ const latest = new Map();
431
+ for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId })) {
432
+ if (receipt.scope === "aggregate" && expected.has(receipt.declaration_id))
433
+ latest.set(receipt.declaration_id, receipt);
434
+ }
435
+ if (latest.size !== expected.size)
436
+ return null;
437
+ const receipts = input.checks.map((check) => latest.get(check.id));
438
+ return receipts.every((receipt) => receipt.status === "passed" && receipt.workspace_fingerprint === current) ? receipts : null;
439
+ }
440
+ /** One aggregate gate per RUN; a second invocation must wait, never duplicate it. */
441
+ function acquireAggregateGateLock(root) {
442
+ const directory = path.join(root, "checks");
443
+ const lock = path.join(directory, ".aggregate-gate.lock");
444
+ fs.mkdirSync(directory, { recursive: true });
445
+ try {
446
+ const descriptor = fs.openSync(lock, "wx");
447
+ fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
448
+ fs.closeSync(descriptor);
449
+ }
450
+ catch (error) {
451
+ if (!isExistingLock(error))
452
+ throw error;
453
+ const activePid = lockPid(lock);
454
+ if (activePid && processAlive(activePid)) {
455
+ throw new AppError("code_gate_in_progress", "CODE aggregate gate is already running; wait for that exact stage finish command to return", 1, { pid: activePid, lock });
456
+ }
457
+ fs.rmSync(lock, { force: true });
458
+ return acquireAggregateGateLock(root);
459
+ }
460
+ return () => fs.rmSync(lock, { force: true });
461
+ }
462
+ function isExistingLock(error) {
463
+ return Boolean(error && typeof error === "object" && error.code === "EEXIST");
464
+ }
465
+ function lockPid(lock) {
466
+ try {
467
+ const parsed = JSON.parse(fs.readFileSync(lock, "utf8"));
468
+ return Number.isInteger(parsed.pid) && Number(parsed.pid) > 0 ? Number(parsed.pid) : null;
469
+ }
470
+ catch {
471
+ return null;
472
+ }
473
+ }
474
+ function processAlive(pid) {
475
+ try {
476
+ process.kill(pid, 0);
477
+ return true;
478
+ }
479
+ catch {
480
+ return false;
481
+ }
482
+ }
413
483
  function verificationProjection(works, receipts) {
414
484
  const packets = works.map((work) => packet(work)).filter((value) => value !== null);
415
485
  const declarations = uniqueBy(packets.flatMap((value) => value.checks), (value) => value.id);
@@ -454,7 +524,7 @@ function packet(work) {
454
524
  return null;
455
525
  try {
456
526
  const value = JSON.parse(work.payload_json);
457
- return value.schema_id === "dd-flow/code-work-packet@4" ? value : null;
527
+ return value.schema_id === "dd-flow/code-work-packet@5" ? value : null;
458
528
  }
459
529
  catch {
460
530
  return null;
@@ -258,7 +258,7 @@ function orchestratorPrompt(context, input) {
258
258
  const decision = path.join(input.root, "decision.json");
259
259
  const revision = currentPlanRevision(input.home, input.run.workspace_root);
260
260
  const workspaceContract = ["<workspace_contract>", `- route: ${input.workspaceRoute.route}`, `- feature branch: ${input.workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${input.workspaceRoute.base_ref ?? "not applicable"}`, `- read/write workspace: ${input.run.workspace_root}`, "The CLI verified this frozen route. All plan and correction writes belong in the named workspace; project root remains only the stable lifecycle identity. Do not create, switch, merge or delete branches/worktrees.", "</workspace_contract>"].join("\n");
261
- return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch requests capacity, run exactly one concurrent fan-out of ${capacityProbeFanoutSize} probes. This measures the harness limit; it is not a task to obtain ${capacityProbeFanoutSize} successful probes. Start #01…#${capacityProbeFanoutSize} once, all together, using all-settled handling so one rejection does not hide the other outcomes. A rejected launch is expected evidence. Never retry, replace, or add a probe. Each started probe calls no tools, reads no files, creates no children, waits ${capacityProbeHoldSeconds} seconds, then returns exactly AGENT-NN. For cleanup, wait at most ${capacityProbeDeadlineSeconds} seconds from the first launch, terminate every unfinished probe, then release every finished probe session that the harness permits. Only after that cleanup record the number of launches that started successfully, not the number of replacement attempts or late completions: ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<successful-initial-launches>")}. Capacity probes are not Works and are never registered.`, "After dispatch, launch at most the measured capacity at once. If more independent reviewer Works remain, wait for the current wave to settle, then start the unchanged queued Works in the next wave. A reviewer launch rejected before it starts is not review evidence: do not create a replacement; wait for a running wave to settle and start that same queued Work. Each reviewer must be a genuinely fresh harness child Session. The lifecycle adapter binds that observed Session; do not bind or supply a Session ID manually. Reviewers are read-only and must not create children. As soon as a reviewer result is accepted, release that reviewer Session when the harness permits; do not let finished disposable workers occupy slots before the next wave.", "Review the execution environment of every selected check as part of its proof: a reset/fixture process, service process and client process must share the intended data and configuration world. A runtime entrypoint that can break that invariant must be assigned to a concrete Work write_scope and ordered before its consumer; required_read alone is not ownership.", "If the final decision needs user input with no reasonable default, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:", "```sh", input.pauseCommandTemplate, "```", "Ask the returned user_message, stop, then resume this same PLAN-REVIEW Work. Do not write decision.json or finish first.", `When all reviewer results are complete and every user question is resolved, classify every material finding, fix accepted findings in this same PLAN-REVIEW Work, then write ${decision} and finish: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer findings use local FIND-NNN ids. dd-flow exposes each finding to this coordinator as WRK-.../FIND-NNN; use that canonical finding_ref in the decision.", "A completed reviewer result with needs_changes or blocked is evidence, not the stage outcome. Classify its material findings and apply accepted fixes in this one review pass; do not start a second review automatically. Only a missing, malformed or unfinished reviewer result blocks the stage. For an accepted correction, increment PLAN revision and update only plan.json and the relevant aspect map. Do not edit or list code-work-batch.json: the CLI validates final PLAN and regenerates it. If no material correction is needed, set correction.status=not_required. The CLI checks mechanical handoff coherence; it does not prove semantic correctness.", "```json", JSON.stringify({ schema_id: "dd-flow/plan-review-decision@3", outcome: "accepted | blocked | failed | cancelled", summary: "Concise evidence-backed final decision.", finding_decisions: [{ finding_ref: "WRK-001-review/FIND-001", decision: "accepted_fix | rejected | deferred_as_DEF | requires_user | duplicate", reason: "Why." }], correction: { status: "not_required | applied", previous_plan_revision: revision, changed_paths: [], summary: "No material correction was needed, or summarize the applied correction." } }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
261
+ return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch requests capacity, run exactly one concurrent fan-out of ${capacityProbeFanoutSize} probes. This measures the harness limit; it is not a task to obtain ${capacityProbeFanoutSize} successful probes. Start #01…#${capacityProbeFanoutSize} once, all together, using all-settled handling so one rejection does not hide the other outcomes. A rejected launch is expected evidence. Never retry, replace, or add a probe. Each started probe calls no tools, reads no files, creates no children, waits ${capacityProbeHoldSeconds} seconds, then returns exactly AGENT-NN. For cleanup, wait at most ${capacityProbeDeadlineSeconds} seconds from the first launch, terminate every unfinished probe, then release every finished probe session that the harness permits. Only after that cleanup record the number of launches that started successfully, not the number of replacement attempts or late completions: ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<successful-initial-launches>")}. Capacity probes are not Works and are never registered.`, "After dispatch, launch at most the measured capacity at once. If more independent reviewer Works remain, wait for the current wave to settle, then start the unchanged queued Works in the next wave. A reviewer launch rejected before it starts is not review evidence: do not create a replacement; wait for a running wave to settle and start that same queued Work. Each reviewer must be a genuinely fresh harness child Session. The lifecycle adapter binds that observed Session; do not bind or supply a Session ID manually. Reviewers are read-only and must not create children. As soon as a reviewer result is accepted, release that reviewer Session when the harness permits; do not let finished disposable workers occupy slots before the next wave.", "Review the execution environment of every selected check as part of its proof: a reset/fixture process, service process and client process must share the intended data and configuration world. A runtime entrypoint that can break that invariant must be explicit in one Work's task and verification and ordered before its consumer. planned_write_areas may advertise likely overlap, but do not treat them as ownership; required_read alone is not a delivery plan.", "If the final decision needs user input with no reasonable default, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:", "```sh", input.pauseCommandTemplate, "```", "Ask the returned user_message, stop, then resume this same PLAN-REVIEW Work. Do not write decision.json or finish first.", `When all reviewer results are complete and every user question is resolved, classify every material finding, fix accepted findings in this same PLAN-REVIEW Work, then write ${decision} and finish: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer findings use local FIND-NNN ids. dd-flow exposes each finding to this coordinator as WRK-.../FIND-NNN; use that canonical finding_ref in the decision.", "A completed reviewer result with needs_changes or blocked is evidence, not the stage outcome. Classify its material findings and apply accepted fixes in this one review pass; do not start a second review automatically. Only a missing, malformed or unfinished reviewer result blocks the stage. For an accepted correction, increment PLAN revision and update only plan.json and the relevant aspect map. Do not edit or list code-work-batch.json: the CLI validates final PLAN and regenerates it. If no material correction is needed, set correction.status=not_required. The CLI checks mechanical handoff coherence; it does not prove semantic correctness.", "```json", JSON.stringify({ schema_id: "dd-flow/plan-review-decision@3", outcome: "accepted | blocked | failed | cancelled", summary: "Concise evidence-backed final decision.", finding_decisions: [{ finding_ref: "WRK-001-review/FIND-001", decision: "accepted_fix | rejected | deferred_as_DEF | requires_user | duplicate", reason: "Why." }], correction: { status: "not_required | applied", previous_plan_revision: revision, changed_paths: [], summary: "No material correction was needed, or summarize the applied correction." } }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
262
262
  }
263
263
  function reviewGroups(home, workspaceRoot) {
264
264
  const root = path.join(home, "03-plan");
@@ -279,7 +279,7 @@ function reviewGroups(home, workspaceRoot) {
279
279
  if (dependencyGroup && dependencyGroup !== key)
280
280
  dependencies.add(dependencyGroup);
281
281
  }
282
- groups.push({ key, protocol_id: protocolId, aspect_ids: group.aspect_ids, depends_on: [...dependencies].sort(), task: `Review PLAN group ${key}: ${group.aspect_ids.join(", ")}. Read run://${path.basename(home)}/03-plan/${protocolId}/aspect-map.json and the referenced plan. Try to falsify ambiguous behavior and missing verification. For every selected check that starts fixture/reset, service, or client processes, verify that they share the intended data/configuration world and that any runtime entrypoint requiring change is in a concrete owner Work write_scope, not merely required_read. Do not edit plan or product files. Return one aspect entry for every assigned aspect, with exact evidence references and concise findings. Finish with the JSON contract supplied in your work prompt.` });
282
+ groups.push({ key, protocol_id: protocolId, aspect_ids: group.aspect_ids, depends_on: [...dependencies].sort(), task: `Review PLAN group ${key}: ${group.aspect_ids.join(", ")}. Read run://${path.basename(home)}/03-plan/${protocolId}/aspect-map.json and the referenced plan. Try to falsify ambiguous behavior and missing verification. For every selected check that starts fixture/reset, service, or client processes, verify that they share the intended data/configuration world and that any runtime entrypoint requiring change is explicit in a Work task and verification and ordered before its consumer. planned_write_areas are coordination hints, not ownership. Do not edit plan or product files. Return one aspect entry for every assigned aspect, with exact evidence references and concise findings. Finish with the JSON contract supplied in your work prompt.` });
283
283
  }
284
284
  }
285
285
  return groups;
@@ -81,14 +81,14 @@ export function startVnextPlan(context, input) {
81
81
  : "- Reviewer capacity is not measured yet. PLAN must not probe or launch reviewers; PLAN-REVIEW will measure it once if review is enabled.";
82
82
  const reviewGroupingRule = "Group only semantically compatible applicable aspects, preserving real trust, irreversible, high-risk and hard-dependency boundaries. Prefer the fewest groups that retain independent review value, normally one review wave. Put two or three compatible aspects in a group; do not create one group per aspect merely for convenience. A later PLAN-REVIEW dispatch measures current capacity once and schedules these semantic groups into waves; do not invent a capacity value here.";
83
83
  const checkProfile = path.join(run.workspace_root, ".memory-bank", "spec", "engineering", "code-check-profile.json");
84
- const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(gitFacts(run.workspace_root))}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(fs.existsSync(checkProfile) ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", fs.readFileSync(checkProfile, "utf8").trim(), "</code_check_policy>", ""] : []), "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", "Use protocol-plan@5. Its top-level checks[] is the single check catalog. Every check has id, command, purpose, run_at and availability. available means executable now. planned means one named P* Work first creates a NEW @check/... alias: planned therefore always needs provided_by and the exact alias definition. Existing @check aliases use the profile and do not repeat a definition. Items and acceptance entries use check_refs only; never duplicate command declarations.", "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an honestly limited external/manual proof. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each plan item must name concrete existing source/test paths in required_read and write_scope. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, assign that path to the Work that owns the change in write_scope; do not leave it merely in required_read or assume another Work will repair it. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision with no reasonable default, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:", "```sh", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
84
+ const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(gitFacts(run.workspace_root))}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(fs.existsSync(checkProfile) ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", fs.readFileSync(checkProfile, "utf8").trim(), "</code_check_policy>", ""] : []), "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", "Use protocol-plan@6. Its top-level checks[] is the single check catalog. Every check has id, command, purpose, run_at and availability. available means executable now. planned means one named P* Work first creates a NEW @check/... alias: planned therefore always needs provided_by and the exact alias definition. Existing @check aliases use the profile and do not repeat a definition. Items and acceptance entries use check_refs only; never duplicate command declarations.", "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an honestly limited external/manual proof. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each plan item must name concrete existing source/test paths in required_read. planned_write_areas is optional: use stable component directories or files only when they help coordinate parallel Work; it is never a write allowlist. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, make that change explicit in the Work task and its verification. Use planned_write_areas only to advertise likely concurrent overlap; do not treat it as ownership or assume another Work will repair an omitted change. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision with no reasonable default, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:", "```sh", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
85
85
  const artifactMaterialization = { status: "materialized", completeness: "partially_filled", plan_paths: planPaths, aspect_map_paths: mapPaths, cli_owned_plan_fields: ["schema_id", "plan_id", "protocol_id", "revision", "source_refs"], cli_owned_aspect_map_fields: ["schema_id", "protocol_id", "plan_id", "plan_revision", "catalog_ref", "aspects[].aspect_id"], validation_commands: validationCommands };
86
86
  const promptPath = path.join(root, "stage-prompt.md");
87
87
  fs.writeFileSync(promptPath, prompt);
88
88
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
89
89
  fs.writeFileSync(path.join(root, "work-context.json"), JSON.stringify({ schema_id: "dd-flow/work-context@1", system: { run_id: run.id, work_id: planWorkId, stage: "plan" }, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root }, artifacts: artifactMaterialization, input: { protocols, owned_obligations: Object.fromEntries(owned) } }, null, 2));
90
90
  const binding = bindStageCoordinatorWork(context, { workId: planWorkId, hookEventId: input.hookEventId, stage: "plan", promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
91
- attachFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@5" });
91
+ attachFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" });
92
92
  const workSessionId = String(binding.work_session_id);
93
93
  const sessionId = String(binding.session_id);
94
94
  refreshRunWorkProjection(context, project.id, run.id);
@@ -208,7 +208,7 @@ export function validateVnextCodeHandoff(context, input) {
208
208
  });
209
209
  }
210
210
  const planTask = "Produce accepted plan.json and aspect-map.json artifacts.";
211
- function planExample(protocolId) { return { schema_id: "dd-flow/protocol-plan@5", plan_id: "PLAN-001", protocol_id: protocolId, revision: 1, title: "Example", summary: "A compact executable plan.", source_refs: [{ kind: "specify", id: "SPECIFY", path: "run://RUN-000/01-specify/specify.json", requirement_ids: ["R-001", "AC-001"] }], goal: { outcome: "Deliver the accepted behavior.", constraints: ["Keep the accepted scope."], non_goals: [] }, assessment: { scope_breadth: { level: "narrow", surfaces: ["one surface"], reason: "One vertical slice." }, solution_novelty: { level: "established", surfaces: ["existing pattern"], reason: "Reuse project practice." }, solution_uncertainty: { level: "low", surfaces: ["known behavior"], reason: "No open technical question." }, failure_impact: { level: "low", surfaces: ["local feature"], reason: "Reversible local change." }, selected_depth: "compact_plan", depth_trigger: "none" }, decisions: [], document_updates: [], checks: [{ id: "CHK-P1-TEST", command: "pnpm test", purpose: "Proves the changed behavior.", run_at: "work", availability: "available" }], items: [{ id: "P1", title: "Implement behavior", summary: "Change the owning surface.", details: "Follow the accepted requirement and project conventions.", depends_on: [], requirement_refs: ["R-001", "AC-001"], semantic_spine: { user_outcome: "The requested behavior is available.", component_responsibility: "Own the behavior.", must_preserve: ["Existing behavior."], non_goals: [], acceptance_contribution: "Makes AC-001 observable." }, execution_context: { required_read: ["apps/api/src/example.ts"], discovery_boundary: ["Related tests only."], write_scope: ["apps/api/src/example.ts"], stop_conditions: ["Stop if accepted scope conflicts with current truth."] }, verification: { check_refs: ["CHK-P1-TEST"] } }], acceptance: [{ criterion_id: "AC-001", plan_item_ids: ["P1"], changed_surfaces: ["apps/api/src/example.ts"], path: "Exercise the accepted user path.", environment: "Local test environment.", fixtures: [], cleanup: "No persistent fixture.", check_refs: ["CHK-P1-TEST"], expected_evidence: ["Focused check passes."], proof_limits: ["Manual production evidence is not claimed."], gate: "work" }] }; }
211
+ function planExample(protocolId) { return { schema_id: "dd-flow/protocol-plan@6", plan_id: "PLAN-001", protocol_id: protocolId, revision: 1, title: "Example", summary: "A compact executable plan.", source_refs: [{ kind: "specify", id: "SPECIFY", path: "run://RUN-000/01-specify/specify.json", requirement_ids: ["R-001", "AC-001"] }], goal: { outcome: "Deliver the accepted behavior.", constraints: ["Keep the accepted scope."], non_goals: [] }, assessment: { scope_breadth: { level: "narrow", surfaces: ["one surface"], reason: "One vertical slice." }, solution_novelty: { level: "established", surfaces: ["existing pattern"], reason: "Reuse project practice." }, solution_uncertainty: { level: "low", surfaces: ["known behavior"], reason: "No open technical question." }, failure_impact: { level: "low", surfaces: ["local feature"], reason: "Reversible local change." }, selected_depth: "compact_plan", depth_trigger: "none" }, decisions: [], document_updates: [], checks: [{ id: "CHK-P1-TEST", command: "pnpm test", purpose: "Proves the changed behavior.", run_at: "work", availability: "available" }], items: [{ id: "P1", title: "Implement behavior", summary: "Change the owning surface.", details: "Follow the accepted requirement and project conventions.", depends_on: [], requirement_refs: ["R-001", "AC-001"], semantic_spine: { user_outcome: "The requested behavior is available.", component_responsibility: "Own the behavior.", must_preserve: ["Existing behavior."], non_goals: [], acceptance_contribution: "Makes AC-001 observable." }, execution_context: { required_read: ["apps/api/src/example.ts"], discovery_boundary: ["Related tests only."], planned_write_areas: ["apps/api/src/"], stop_conditions: ["Stop if accepted scope conflicts with current truth."] }, verification: { check_refs: ["CHK-P1-TEST"] } }], acceptance: [{ criterion_id: "AC-001", plan_item_ids: ["P1"], changed_surfaces: ["apps/api/src/example.ts"], path: "Exercise the accepted user path.", environment: "Local test environment.", fixtures: [], cleanup: "No persistent fixture.", check_refs: ["CHK-P1-TEST"], expected_evidence: ["Focused check passes."], proof_limits: ["Manual production evidence is not claimed."], gate: "work" }] }; }
212
212
  function aspectMapExample(protocolId) { return { $schema: "plan-aspect-map.schema.json", schema_id: "dd-flow/plan-aspect-map@3", protocol_id: protocolId, plan_id: "PLAN-001", plan_revision: 1, catalog_ref: { path: ".memory-bank/dd-flow/mb-sdlc/plan-aspects/aspects" }, routing: { initial_state: "orchestrator_local", selected_route: "local_compact", reason: "One genuinely small semantic unit.", groups: [] }, review_groups: [], aspects: [{ aspect_id: "example_aspect", applicability: "not_applicable", reason: "Only an example; use the supplied real catalog.", planned_artifact_refs: [] }] }; }
213
213
  function requireRun(context, root, id) { const project = requireProjectByRoot(context, root); const run = context.db.get("SELECT id, project_id, workspace_root, run_home_path FROM runs WHERE project_id = ? AND id = ?", [project.id, id]); if (!run)
214
214
  throw new AppError("not_found", "RUN is not registered", 1); return run; }
@@ -304,7 +304,7 @@ function protocolOwnership(home, protocols) {
304
304
  function planIdentity(home, runId, protocolId, owned) {
305
305
  const specify = path.join(home, "01-specify", "specify.json");
306
306
  return {
307
- schema_id: "dd-flow/protocol-plan@5",
307
+ schema_id: "dd-flow/protocol-plan@6",
308
308
  plan_id: `PLAN-${protocolId.slice(4)}`,
309
309
  protocol_id: protocolId,
310
310
  revision: 1,
@@ -377,7 +377,7 @@ function projectCodeWorkBatch(input) {
377
377
  const documentPaths = documentUpdates.map((entry) => entry.path);
378
378
  const existingDocumentPaths = documentUpdates.filter((entry) => entry.action === "update").map((entry) => entry.path);
379
379
  return ({
380
- schema_id: "dd-flow/code-work-packet@4",
380
+ schema_id: "dd-flow/code-work-packet@5",
381
381
  key: workKey,
382
382
  launch_policy: "fresh_agent_required",
383
383
  source: { plan_id: value.plan_id, protocol_id: protocolId, plan_item_id: item.id, revision: value.revision, sha256: checksum(file) },
@@ -401,7 +401,7 @@ function projectCodeWorkBatch(input) {
401
401
  : [])
402
402
  ])],
403
403
  discovery_boundary: item.execution_context.discovery_boundary,
404
- write_scope: [...new Set([...item.execution_context.write_scope, ...documentPaths])],
404
+ planned_write_areas: [...new Set([...item.execution_context.planned_write_areas, ...documentPaths])],
405
405
  checks: value.checks.filter((check) => item.verification.check_refs.includes(check.id)),
406
406
  provides_checks: value.checks.filter((check) => check.availability === "planned" && check.provided_by === item.id),
407
407
  stop_conditions: item.execution_context.stop_conditions,
@@ -429,39 +429,21 @@ function projectCodeWorkBatch(input) {
429
429
  }
430
430
  }
431
431
  const ordered = [...works].sort((a, b) => a.key.localeCompare(b.key));
432
- return { schema_id: "dd-flow/code-work-batch@4", sources: input.plans.map(({ protocolId, file, value }) => ({ plan_id: value.plan_id, protocol_id: protocolId, revision: value.revision, sha256: checksum(file) })), works: ordered };
432
+ return { schema_id: "dd-flow/code-work-batch@5", sources: input.plans.map(({ protocolId, file, value }) => ({ plan_id: value.plan_id, protocol_id: protocolId, revision: value.revision, sha256: checksum(file) })), works: ordered };
433
433
  }
434
434
  function renderCodeTask(item, catalog) { const checks = catalog.filter((check) => item.verification.check_refs.includes(check.id)); return [item.title, item.summary, item.details, `Preserve: ${item.semantic_spine.must_preserve.join("; ")}`, `Checks: ${checks.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Stop: ${item.execution_context.stop_conditions.join("; ")}`].join("\n\n"); }
435
435
  function validateProjectedPaths(batch, workspaceRoot, home, runId) {
436
- const byKey = new Map(batch.works.map((work) => [work.key, work]));
437
- const ancestors = (key, seen = new Set()) => { for (const dependency of byKey.get(key)?.depends_on ?? [])
438
- if (!seen.has(dependency)) {
439
- seen.add(dependency);
440
- ancestors(dependency, seen);
441
- } return seen; };
442
436
  for (const work of batch.works) {
443
- for (const write of work.write_scope)
444
- assertProjectPath(write, workspaceRoot, "write_scope");
437
+ for (const write of work.planned_write_areas)
438
+ assertProjectPath(write, workspaceRoot, "planned_write_areas");
445
439
  for (const read of work.required_read) {
446
440
  const existing = resolvePortablePath(read, workspaceRoot, home, runId);
447
441
  if (existing && fs.existsSync(existing))
448
442
  continue;
449
- const producers = [...ancestors(work.key)].map((key) => byKey.get(key)).filter((candidate) => candidate.write_scope.includes(read));
450
- if (!producers.length)
451
- throw new AppError("validation", "CODE read path must exist at entry or be written by an ordered predecessor", 2, { work: work.key, read_path: read });
443
+ throw new AppError("validation", "CODE required_read path must exist at Work entry", 2, { work: work.key, read_path: read });
452
444
  }
453
445
  }
454
- for (let index = 0; index < batch.works.length; index += 1)
455
- for (const other of batch.works.slice(index + 1)) {
456
- const work = batch.works[index];
457
- for (const pathValue of work.write_scope.filter((value) => other.write_scope.some((otherPath) => pathsOverlap(value, otherPath)))) {
458
- const ordered = ancestors(work.key).has(other.key) || ancestors(other.key).has(work.key);
459
- if (!ordered)
460
- throw new AppError("validation", "Several CODE Works write the same path without graph ordering", 2, { path: pathValue, works: [work.key, other.key] });
461
- }
462
- }
463
446
  }
464
- function pathsOverlap(left, right) { return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`); }
465
447
  function acceptedObligationMap(home) {
466
448
  const specify = readVnextSpecifyResult(path.join(home, "01-specify", "specify.json"));
467
449
  const effective = new Map([...specify.requirements, ...specify.acceptance_criteria].map((item) => [item.id, item.statement]));
@@ -513,12 +495,6 @@ function validatePlanSemantics(file, ownedRefs, acceptedRefs) {
513
495
  throw new AppError("planned_check_requires_alias", "A planned check must declare a new @check/... alias", 2, { file, check_id: check.id, command: check.command });
514
496
  if (check.availability === "planned" && !check.definition)
515
497
  throw new AppError("planned_check_definition_missing", "A planned check must declare its exact alias definition", 2, { file, check_id: check.id });
516
- if (check.availability === "planned") {
517
- const provider = items.find((item) => item.id === check.provided_by);
518
- if (!(provider.execution_context.write_scope ?? []).some((scope) => scope === ".memory-bank/spec/engineering/code-check-profile.json")) {
519
- throw new AppError("planned_check_provider_scope_missing", "The planned check provider must own code-check-profile.json", 2, { file, check_id: check.id, provider: check.provided_by });
520
- }
521
- }
522
498
  checks.set(check.id, check);
523
499
  }
524
500
  const ancestors = (itemId, seen = new Set()) => {
@@ -137,7 +137,7 @@ export function startWork(context, id, input) {
137
137
  if (!isReady(context, work)) {
138
138
  const blockers = readinessBlockers(context, work);
139
139
  const ready = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, start_command: workStartCommand(context, candidate) }));
140
- throw new AppError("work_not_ready", "Work cannot start until dependencies complete and overlapping write scopes are free", 2, { work_id: id, blockers, ready });
140
+ throw new AppError("work_not_ready", "Work cannot start until dependencies complete and overlapping planned coordination areas are free", 2, { work_id: id, blockers, ready });
141
141
  }
142
142
  const run = requireRun(context, work.project_id, work.run_id);
143
143
  if (input.projectRoot && resolveProjectRoot(input.projectRoot) !== resolveProjectRoot(run.project_root))
@@ -165,8 +165,31 @@ export function bindRunningWorkSession(context, input) {
165
165
  const identity = hookSessionIdentity(context, work.project_id, input.hookEventId);
166
166
  const active = context.db.get("SELECT id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, completed_at FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [work.work_id]);
167
167
  if (active) {
168
- if (active.session_id !== identity.sessionId)
169
- throw new AppError("handoff_session_mismatch", "The active Work is already bound to a different Session", 1, { work_id: work.work_id });
168
+ if (active.session_id !== identity.sessionId) {
169
+ // A frozen `new_session` policy may transfer the same coordinator Work
170
+ // only to its next stage. Repeating the same stage from another Session
171
+ // remains a conflict because its result path is unchanged.
172
+ if (!input.allowStageHandoff || path.resolve(active.result_path ?? "") === path.resolve(input.resultPath ?? ""))
173
+ throw new AppError("handoff_session_mismatch", "The active Work is already bound to a different Session", 1, { work_id: work.work_id });
174
+ const now = context.now();
175
+ const id = `WS-${crypto.randomUUID()}`;
176
+ context.db.exec("BEGIN IMMEDIATE");
177
+ try {
178
+ context.db.run("UPDATE work_sessions SET status = 'completed', completed_at = ?, updated_at = ? WHERE id = ?", [now, now, active.id]);
179
+ const stillRunning = context.db.get("SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running' LIMIT 1", [active.session_id]);
180
+ if (!stillRunning)
181
+ context.db.run("UPDATE sessions SET status = 'idle', updated_at = ? WHERE project_id = ? AND session_id = ?", [now, work.project_id, active.session_id]);
182
+ bindSession(context, work, run, identity, now);
183
+ context.db.run("INSERT INTO work_sessions (id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, NULL)", [id, work.work_id, identity.sessionId, input.hookEventId, input.promptPath, input.resultPath ?? null, now, now]);
184
+ context.db.exec("COMMIT");
185
+ }
186
+ catch (error) {
187
+ context.db.exec("ROLLBACK");
188
+ throw error;
189
+ }
190
+ refreshRunWorkProjection(context, work.project_id, work.run_id);
191
+ return { work_session_id: id, session_id: identity.sessionId, handed_off: true };
192
+ }
170
193
  context.db.run("UPDATE work_sessions SET prompt_path = ?, result_path = COALESCE(?, result_path), updated_at = ? WHERE id = ?", [input.promptPath, input.resultPath ?? null, context.now(), active.id]);
171
194
  return { work_session_id: active.id, session_id: identity.sessionId, reused: true };
172
195
  }
@@ -190,7 +213,8 @@ export function bindStageCoordinatorWork(context, input) {
190
213
  const work = requireWork(context, input.workId);
191
214
  const run = requireRun(context, work.project_id, work.run_id);
192
215
  claimStageStartHookEvent(context, { projectId: work.project_id, eventKey: input.hookEventId, runId: work.run_id, stage: input.stage, projectRoot: run.project_root, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
193
- const binding = bindRunningWorkSession(context, input);
216
+ const snapshot = JSON.parse((context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id])?.index_json) ?? "{}");
217
+ const binding = bindRunningWorkSession(context, { ...input, allowStageHandoff: snapshot.execution_profile?.settings?.stage_session_mode === "new_session" });
194
218
  context.db.run("UPDATE sessions SET current_stage = ?, updated_at = ? WHERE project_id = ? AND session_id = ?", [input.stage, context.now(), work.project_id, binding.session_id]);
195
219
  refreshRunSessionProjection(context, work.project_id, work.run_id);
196
220
  return binding;
@@ -261,11 +285,12 @@ async function settle(context, id, status, result, progress) {
261
285
  if (work.status === "running" && !link)
262
286
  throw new AppError("runtime_missing", "Running Work has no open Work/Session link", 1, { work_id: id });
263
287
  let receipts = [];
288
+ let coordinationDrift = [];
264
289
  if (status === "completed") {
265
290
  validateWorkResult(work, result, run.project_root, run.workspace_root, run.id, link?.result_path ?? null);
266
291
  const packet = codePacket(work);
267
292
  if (packet) {
268
- validateChangedPaths(packet, result);
293
+ coordinationDrift = plannedAreaDrift(packet, result);
269
294
  const artifactDir = path.relative(requireRunHome(run), path.dirname(link.result_path));
270
295
  receipts = await runCodeChecks(context, { projectId: work.project_id, runId: work.run_id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, workId: work.work_id, artifactDir, scope: "work", checks: packet.checks.filter((check) => check.run_at === "work"), ...(progress ? { progress } : {}) });
271
296
  const failed = receipts.filter((receipt) => receipt.status === "failed");
@@ -293,18 +318,16 @@ async function settle(context, id, status, result, progress) {
293
318
  }
294
319
  refreshRunWorkProjection(context, work.project_id, work.run_id);
295
320
  const newlyReady = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, start_command: workStartCommand(context, candidate) }));
296
- appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: `work_${status}`, work_id: work.work_id, session_id: link?.session_id ?? null });
321
+ appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: `work_${status}`, work_id: work.work_id, session_id: link?.session_id ?? null, ...(coordinationDrift.length ? { coordination_drift: coordinationDrift } : {}) });
297
322
  for (const ready of newlyReady)
298
323
  appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_dependency_unblocked", work_id: ready.work_id, completed_dependency: work.work_id });
299
- return { ok: true, work_id: id, status, checks: receipts, newly_ready: newlyReady, graph: codeWorkGraph(context, work.project_id, work.run_id), usage: { status: "provisional", reason: "final usage is controller-owned" } };
324
+ return { ok: true, work_id: id, status, checks: receipts, coordination: { planned_write_areas: codePacket(work)?.planned_write_areas ?? [], drift: coordinationDrift }, newly_ready: newlyReady, graph: codeWorkGraph(context, work.project_id, work.run_id), usage: { status: "provisional", reason: "final usage is controller-owned" } };
300
325
  }
301
- function validateChangedPaths(packet, result) {
326
+ function plannedAreaDrift(packet, result) {
302
327
  const changed = JSON.parse(result).changed_paths;
303
- if (!Array.isArray(changed))
304
- return;
305
- const outside = changed.filter((item) => typeof item === "string" && !packet.write_scope.some((scope) => item === scope || item.startsWith(`${scope}/`)));
306
- if (outside.length)
307
- throw new AppError("write_scope_violation", "CODE Work result declares paths outside its accepted write scope", 2, { outside, write_scope: packet.write_scope });
328
+ if (!Array.isArray(changed) || packet.planned_write_areas.length === 0)
329
+ return [];
330
+ return changed.filter((item) => typeof item === "string" && !packet.planned_write_areas.some((area) => item === area || item.startsWith(`${area}/`)));
308
331
  }
309
332
  function bindSession(context, work, run, identity, now) {
310
333
  const priorWorkSession = context.db.get("SELECT session_id FROM work_sessions WHERE work_id = ? ORDER BY created_at DESC LIMIT 1", [work.work_id])?.session_id ?? null;
@@ -372,10 +395,10 @@ function validateCodeWorkResult(work, value, projectRoot) {
372
395
  function renderWorkerPrompt(context, work, run, dependencies, resultPath) {
373
396
  const command = `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} dd-flow`;
374
397
  const packet = codePacket(work);
375
- const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete only this Work's semantic_spine.acceptance_contribution, task, write_scope, and declared checks. A criterion may name another ordered Work's UI, documentation, or evidence surface; do not fail merely because that surface is outside this Work's write_scope.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<write_scope>", ...packet.write_scope.map((item) => `- ${item}`), "</write_scope>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies the materialization and then executes the check.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
398
+ const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies the materialization and then executes the check.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
376
399
  if (packet)
377
- codeContext.push("<document_updates>", JSON.stringify(packet.document_updates, null, 2), "Materialize every listed update. dd-flow verifies the resulting file against its PLAN-time baseline.", "</document_updates>", "", "<contract_failure>", "Successful completion requires empty deviations and blockers and every assigned document update in changed_paths. If a required path is missing from write_scope, do not report success: fail this Work with that exact path and the retained receipt so the coordinator can correct the packet.", "</contract_failure>", "");
378
- return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<workspace_contract>", `All project reads and writes belong under ${run.workspace_root}. Project root is only the stable lifecycle identity for dd-flow commands. Do not create, switch, merge or delete branches/worktrees.`, "</workspace_contract>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work), `Write it to ${resultPath}.`, "</result_contract>", ""] : []), "<completion>", "The CLI runs every declared required check before accepting this Work. A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", "Read the failed receipt and its stdout/stderr. Fix project-owned source, migration, test, formatting, or configuration errors in this same Work, then call Finish again. Do not invent a cause that does not appear in the retained output.", "Use Fail only for a concrete external blocker after deterministic bootstrap, or for a contractual scope conflict where a required repair path lies outside this Work's write_scope. Quote the exact receipt plus the error or missing path. A project check failure inside this Work's scope must be fixed before Finish.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `Finish as one standalone command: ${command} work finish ${work.work_id} --result-file ${JSON.stringify(resultPath)} --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`, `Fail only for an evidenced external or scope-contract case: ${command} work fail ${work.work_id} --reason "receipt path + exact error or missing write_scope path" --project-root ${JSON.stringify(run.project_root)} --json`, "</completion>", ""].join("\n");
400
+ codeContext.push("<document_updates>", JSON.stringify(packet.document_updates, null, 2), "Materialize every listed update. dd-flow verifies the resulting file against its PLAN-time baseline.", "</document_updates>", "", "<completion_contract>", "Successful completion requires empty deviations and blockers and every assigned document update in changed_paths. A necessary path outside planned_write_areas is normal coordination drift, not a blocker; include it in changed_paths and continue.", "</completion_contract>", "");
401
+ return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<hard_write_boundary>", `HARD RULE: all project reads and writes must remain under ${run.workspace_root}.`, "Do not write through project_root, outside workspace_root, into another RUN, or into Git/worktree control data. Do not create, switch, merge or delete branches/worktrees.", "Accepted requirements, non-goals and stop_conditions are semantic hard boundaries. planned_write_areas is not.", "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work), `Write it to ${resultPath}.`, "</result_contract>", ""] : []), "<completion>", "The CLI runs every declared required check before accepting this Work. A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", "Read the failed receipt and its stdout/stderr. Fix project-owned source, migration, test, formatting, or configuration errors in this same Work, then call Finish again. Do not invent a cause that does not appear in the retained output.", "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `Finish as one standalone command: ${command} work finish ${work.work_id} --result-file ${JSON.stringify(resultPath)} --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`, `Fail only for an evidenced external or semantic-contract blocker: ${command} work fail ${work.work_id} --reason "receipt path + exact external or semantic blocker" --project-root ${JSON.stringify(run.project_root)} --json`, "</completion>", ""].join("\n");
379
402
  }
380
403
  function resultSchemaGuidance(work) {
381
404
  const schema = work.result_schema;
@@ -435,27 +458,27 @@ catch {
435
458
  function isReady(context, work) { return work.status === "created" && readinessBlockers(context, work).length === 0; }
436
459
  function readinessBlockers(context, work) {
437
460
  const blockers = parseDependencies(work).flatMap((id) => { const dependency = context.db.get("SELECT status FROM works WHERE work_id = ?", [id]); return dependency?.status === "completed" ? [] : [{ kind: "dependency", work_id: id, status: dependency?.status ?? "missing" }]; });
438
- const scope = workWriteScope(work);
439
- if (scope.length === 0)
461
+ const areas = workPlannedWriteAreas(work);
462
+ if (areas.length === 0)
440
463
  return blockers;
441
464
  for (const running of context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'running' AND work_id != ?`, [work.project_id, work.run_id, work.work_id])) {
442
- const overlap = overlappingScopes(scope, workWriteScope(running));
465
+ const overlap = overlappingAreas(areas, workPlannedWriteAreas(running));
443
466
  if (overlap.length)
444
- blockers.push({ kind: "write_scope", work_id: running.work_id, paths: overlap });
467
+ blockers.push({ kind: "planned_write_area", work_id: running.work_id, paths: overlap });
445
468
  }
446
469
  return blockers;
447
470
  }
448
- function workWriteScope(work) { const payload = parsePayload(work); return Array.isArray(payload?.write_scope) ? payload.write_scope.filter((item) => typeof item === "string").map((item) => item.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "")) : []; }
449
- function overlappingScopes(left, right) { const overlap = new Set(); for (const a of left)
471
+ function workPlannedWriteAreas(work) { const payload = parsePayload(work); return Array.isArray(payload?.planned_write_areas) ? payload.planned_write_areas.filter((item) => typeof item === "string").map((item) => item.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "")) : []; }
472
+ function overlappingAreas(left, right) { const overlap = new Set(); for (const a of left)
450
473
  for (const b of right)
451
474
  if (a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`))
452
475
  overlap.add(a.length <= b.length ? a : b); return [...overlap]; }
453
476
  function validateItem(value, requireExecutionContext = false) { if (!value || typeof value !== "object" || Array.isArray(value))
454
477
  throw new AppError("validation", "work item must be an object", 2); const item = value; if (typeof item.key !== "string" || !item.key || typeof item.task !== "string" || !item.task.trim())
455
- throw new AppError("validation", "work item requires key and task", 2); const code = item.schema_id === "dd-flow/code-work-packet@4"; if (requireExecutionContext && !code)
456
- throw new AppError("validation", "CODE batch requires code-work-packet@4 items", 2, { key: item.key }); for (const key of code ? ["required_read", "discovery_boundary", "write_scope", "checks", "provides_checks", "stop_conditions"] : [])
457
- if (!Array.isArray(item[key]) || (key !== "provides_checks" && item[key].length === 0))
458
- throw new AppError("validation", `CODE work item requires ${key === "provides_checks" ? "an array" : `non-empty ${key}`}`, 2, { key: item.key }); if (item.depends_on !== undefined && (!Array.isArray(item.depends_on) || !item.depends_on.every((entry) => typeof entry === "string")))
478
+ throw new AppError("validation", "work item requires key and task", 2); const code = item.schema_id === "dd-flow/code-work-packet@5"; if (requireExecutionContext && !code)
479
+ throw new AppError("validation", "CODE batch requires code-work-packet@5 items", 2, { key: item.key }); for (const key of code ? ["required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions"] : [])
480
+ if (!Array.isArray(item[key]) || (!['provides_checks', 'planned_write_areas'].includes(key) && item[key].length === 0))
481
+ throw new AppError("validation", `CODE work item requires ${['provides_checks', 'planned_write_areas'].includes(key) ? "an array" : `non-empty ${key}`}`, 2, { key: item.key }); if (item.depends_on !== undefined && (!Array.isArray(item.depends_on) || !item.depends_on.every((entry) => typeof entry === "string")))
459
482
  throw new AppError("validation", "depends_on must be a string array", 2); if (item.parent !== undefined && typeof item.parent !== "string")
460
483
  throw new AppError("validation", "parent must be a string", 2); if (item.launch_policy !== undefined && item.launch_policy !== "reuse_allowed" && item.launch_policy !== "fresh_agent_required")
461
484
  throw new AppError("validation", "launch_policy must be reuse_allowed or fresh_agent_required", 2); if (item.result_schema !== undefined && (typeof item.result_schema !== "string" || !item.result_schema))
@@ -480,7 +503,7 @@ function parsePayload(work) { if (!work.payload_json)
480
503
  catch {
481
504
  return null;
482
505
  } }
483
- function codePacket(work) { const value = parsePayload(work); if (value?.schema_id !== "dd-flow/code-work-packet@4")
506
+ function codePacket(work) { const value = parsePayload(work); if (value?.schema_id !== "dd-flow/code-work-packet@5")
484
507
  return null; return value; }
485
508
  function assertNoCycles(nodes) { const local = new Map(nodes.map((node) => [node.id, node.dependencies.filter((dependency) => nodes.some((candidate) => candidate.id === dependency))])); const active = new Set(); const done = new Set(); const visit = (id) => { if (active.has(id))
486
509
  throw new AppError("validation", "Work dependencies contain a cycle", 2); if (done.has(id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.8.0-beta.135",
3
+ "version": "0.8.0",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {