@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 +36 -0
- package/README.md +1 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +2 -2
- package/dist/cli/run-cli.js +0 -2
- package/dist/domain/validation.js +1 -1
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/compatibility.schema.json +32 -0
- package/dist/schemas/flow-contract.schema.json +3 -2
- package/dist/schemas/protocol-plan.schema.json +1 -1
- package/dist/schemas/stage-start-response.schema.json +4 -2
- package/dist/schemas/status-report.schema.json +76 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +3 -3
- package/dist/services/code-checks.js +1 -1
- package/dist/services/prompts.js +16 -10
- package/dist/services/vnext-code-review.js +2 -5
- package/dist/services/vnext-code.js +89 -19
- package/dist/services/vnext-plan-review.js +2 -2
- package/dist/services/vnext-plan.js +10 -34
- package/dist/services/work-registry.js +50 -27
- package/package.json +1 -1
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/
|
|
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
|
|
package/dist/build-info.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.8.0
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-08-
|
|
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":
|
|
8
|
-
"commit":
|
|
9
|
-
"flow_contract":
|
|
10
|
-
"repo_root":
|
|
11
|
-
"memorybank_root":
|
|
12
|
-
"flow_root":
|
|
13
|
-
"layout":
|
|
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>...
|
|
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.
|
|
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",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -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
|
-
|
|
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@
|
|
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@
|
|
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", "
|
|
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": {"
|
|
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", "
|
|
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", "
|
|
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
|
-
"
|
|
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@
|
|
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@
|
|
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", "
|
|
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
|
|
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));
|
package/dist/services/prompts.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
"##
|
|
294
|
-
`-
|
|
295
|
-
|
|
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
|
-
"##
|
|
311
|
+
"## Discovery Hints",
|
|
311
312
|
...input.discoveryBoundary.map((value) => `- \`${value}\``),
|
|
312
313
|
"",
|
|
313
|
-
"
|
|
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
|
|
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@
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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@
|
|
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
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
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
|
|
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@
|
|
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
|
|
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
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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
|
-
|
|
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@
|
|
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.
|
|
444
|
-
assertProjectPath(write, workspaceRoot, "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
326
|
+
function plannedAreaDrift(packet, result) {
|
|
302
327
|
const changed = JSON.parse(result).changed_paths;
|
|
303
|
-
if (!Array.isArray(changed))
|
|
304
|
-
return;
|
|
305
|
-
|
|
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
|
|
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>", "", "<
|
|
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>", "", "<
|
|
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
|
|
439
|
-
if (
|
|
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 =
|
|
465
|
+
const overlap = overlappingAreas(areas, workPlannedWriteAreas(running));
|
|
443
466
|
if (overlap.length)
|
|
444
|
-
blockers.push({ kind: "
|
|
467
|
+
blockers.push({ kind: "planned_write_area", work_id: running.work_id, paths: overlap });
|
|
445
468
|
}
|
|
446
469
|
return blockers;
|
|
447
470
|
}
|
|
448
|
-
function
|
|
449
|
-
function
|
|
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@
|
|
456
|
-
throw new AppError("validation", "CODE batch requires code-work-packet@
|
|
457
|
-
if (!Array.isArray(item[key]) || (key
|
|
458
|
-
throw new AppError("validation", `CODE work item requires ${key
|
|
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@
|
|
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))
|