@cassiomc1/forgeloop 0.1.2 → 0.1.3
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/.cursor/rules/project-loop.mdc +5 -0
- package/.github/copilot-instructions.md +5 -0
- package/AGENTS.md +5 -1
- package/CLAUDE.md +5 -1
- package/LOOP_ENGINEERING.md +30 -6
- package/QUALITY_SCORECARD.md +12 -0
- package/README.md +21 -0
- package/conformance/README.md +8 -3
- package/package.json +1 -1
- package/src/cli.js +25 -6
- package/src/commands/next.js +29 -0
- package/src/core/completion-artifacts.js +49 -9
- package/src/core/completion-relationships.js +141 -0
- package/src/core/completion.js +33 -62
- package/src/core/events.js +34 -0
- package/src/core/execution-prerequisites.js +200 -0
- package/src/core/next-action.js +709 -0
- package/src/core/phase.js +142 -18
- package/src/core/preflight.js +165 -2
|
@@ -14,6 +14,11 @@ alwaysApply: true
|
|
|
14
14
|
7. Diagnose failures before fixing them and do not repeat an attempt without new evidence.
|
|
15
15
|
8. Request authority for unrelated external or destructive actions, installations, and publications. For a task-scoped missing Qwen-MM-Plugins capability, follow `LOOP_ENGINEERING.md`; API credentials and system dependencies remain separately gated.
|
|
16
16
|
9. After implementation begins, do not return a final result in `EXECUTING`: follow `VERIFYING` → structured evidence → `REVIEWING` → execution receipt → validator-backed `COMPLETE`; otherwise report `BLOCKED` or `PARTIALLY VERIFIED` with exact findings.
|
|
17
|
+
|
|
18
|
+
After implementation work for the current task is complete, run `forgeloop next`
|
|
19
|
+
before returning a final result. Follow the returned lifecycle action until
|
|
20
|
+
ForgeLoop reaches a terminal state or an explicit blocker.
|
|
21
|
+
|
|
17
22
|
10. Report only demonstrated results and checks.
|
|
18
23
|
|
|
19
24
|
Do not stop for non-blocking missing product details. When a safe, reversible
|
|
@@ -12,6 +12,11 @@ directory or file rules.
|
|
|
12
12
|
- Diagnose the cause before fixing a failure; do not make random attempts.
|
|
13
13
|
- Do not install unrelated software, publish, delete, migrate, or alter external state without authority. For a task-scoped missing Qwen-MM-Plugins capability, follow `LOOP_ENGINEERING.md`; API credentials and system dependencies remain separately gated.
|
|
14
14
|
- After implementation begins, do not return a final result in `EXECUTING`: follow `VERIFYING` → structured evidence → `REVIEWING` → execution receipt → validator-backed `COMPLETE`; otherwise report `BLOCKED` or `PARTIALLY VERIFIED` with exact findings.
|
|
15
|
+
|
|
16
|
+
After implementation work for the current task is complete, run `forgeloop next`
|
|
17
|
+
before returning a final result. Follow the returned lifecycle action until
|
|
18
|
+
ForgeLoop reaches a terminal state or an explicit blocker.
|
|
19
|
+
|
|
15
20
|
- Report the result, checks actually run, limitations, and publication state.
|
|
16
21
|
|
|
17
22
|
Do not stop for non-blocking missing product details. When a safe, reversible
|
package/AGENTS.md
CHANGED
|
@@ -14,7 +14,11 @@ specific rule overrides them.
|
|
|
14
14
|
9. Before implementation, create/validate `.forgeloop/current-contract.json`, persist deterministic routing, satisfy mandatory gates, and require `forgeloop preflight` to return `READY`.
|
|
15
15
|
10. Before claiming `COMPLETE`, require `forgeloop complete` to return `VALID`; otherwise report completion as not protocol-verified.
|
|
16
16
|
11. After implementation begins, do not return a final result in `EXECUTING`: advance through `VERIFYING`, record structured evidence, reach `REVIEWING`, prepare/update the execution receipt, and require `forgeloop complete` to return `VALID`. If closure cannot be reached, report `BLOCKED` or `PARTIALLY VERIFIED` with exact findings.
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
12. After implementation work for the current task is complete, run `forgeloop next`
|
|
19
|
+
before returning a final result. Follow the returned lifecycle action until
|
|
20
|
+
ForgeLoop reaches a terminal state or an explicit blocker.
|
|
21
|
+
13. Finish with the result, checks actually run, limitations, and publication state.
|
|
18
22
|
|
|
19
23
|
Do not stop for non-blocking missing product details. When a safe, reversible
|
|
20
24
|
local default exists, record it as an agent assumption and follow the Blocking vs Non-Blocking Decisions policy in `LOOP_ENGINEERING.md`. Ask only for
|
package/CLAUDE.md
CHANGED
|
@@ -10,7 +10,11 @@ When working in this repository:
|
|
|
10
10
|
6. Investigate the root cause of a failure before fixing it.
|
|
11
11
|
7. Request authority for unrelated installation, publication, deletion, migration, or another external or destructive action. For a task-scoped missing Qwen-MM-Plugins capability, follow `LOOP_ENGINEERING.md`; API credentials and system dependencies remain separately gated.
|
|
12
12
|
8. After implementation begins, do not return a final result in `EXECUTING`: follow `VERIFYING` → structured evidence → `REVIEWING` → execution receipt → validator-backed `COMPLETE`; otherwise report `BLOCKED` or `PARTIALLY VERIFIED` with exact findings.
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
9. After implementation work for the current task is complete, run `forgeloop next`
|
|
15
|
+
before returning a final result. Follow the returned lifecycle action until
|
|
16
|
+
ForgeLoop reaches a terminal state or an explicit blocker.
|
|
17
|
+
10. Report current evidence, limitations, and publication state without claiming checks that were not run.
|
|
14
18
|
|
|
15
19
|
Do not stop for non-blocking missing product details. When a safe, reversible
|
|
16
20
|
local default exists, record it as an agent assumption and follow the Blocking vs Non-Blocking Decisions policy in `LOOP_ENGINEERING.md`. Ask only for
|
package/LOOP_ENGINEERING.md
CHANGED
|
@@ -72,20 +72,44 @@ Implementation finished is not task finished, and tests executed are not the
|
|
|
72
72
|
same as verification recorded. After implementation begins, do not stop or
|
|
73
73
|
return the final result while the ForgeLoop task remains in `EXECUTING`.
|
|
74
74
|
|
|
75
|
+
ACT → QUERY NEXT → ACT → QUERY NEXT → … → TERMINAL
|
|
76
|
+
|
|
77
|
+
At each lifecycle boundary, query persisted state with `forgeloop next` before
|
|
78
|
+
choosing the next action. Always query after implementation, verification,
|
|
79
|
+
correction, and review. The query is advisory and read-only; the host agent
|
|
80
|
+
runs checks and applies the returned legal transition or repair action.
|
|
81
|
+
|
|
75
82
|
```text
|
|
76
83
|
POST-IMPLEMENTATION CLOSURE
|
|
77
84
|
|
|
78
85
|
EXECUTING
|
|
79
|
-
↓
|
|
80
|
-
VERIFYING
|
|
81
|
-
↓
|
|
82
|
-
|
|
83
|
-
↓
|
|
84
|
-
|
|
86
|
+
↓ forgeloop next
|
|
87
|
+
advance --to VERIFYING
|
|
88
|
+
↓ forgeloop next
|
|
89
|
+
prepare-completion
|
|
90
|
+
↓
|
|
91
|
+
run applicable project checks
|
|
92
|
+
↓
|
|
93
|
+
record observed results with record-check
|
|
94
|
+
↓ forgeloop next
|
|
95
|
+
advance --to REVIEWING
|
|
96
|
+
↓ forgeloop next
|
|
97
|
+
complete
|
|
85
98
|
↓ VALID
|
|
86
99
|
COMPLETE
|
|
87
100
|
```
|
|
88
101
|
|
|
102
|
+
`prepare-completion` creates or refreshes the in-progress execution receipt.
|
|
103
|
+
It does not claim completion. The receipt is the structured container used by
|
|
104
|
+
subsequent `record-check` operations; completion remains invalid until required
|
|
105
|
+
observed evidence, review state, chronology, and validator requirements are
|
|
106
|
+
satisfied.
|
|
107
|
+
|
|
108
|
+
The host agent runs applicable checks after the receipt exists, records their
|
|
109
|
+
observed results with `record-check`, and queries `forgeloop next` before each
|
|
110
|
+
subsequent lifecycle action. `record-check` records results already observed by
|
|
111
|
+
the agent; it never executes the supplied command text.
|
|
112
|
+
|
|
89
113
|
Continue until the terminal outcome is either validator-backed `COMPLETE` or
|
|
90
114
|
an explicitly reported `BLOCKED` / `PARTIALLY VERIFIED` result with exact
|
|
91
115
|
unresolved findings. Record verification evidence before review. If a required
|
package/QUALITY_SCORECARD.md
CHANGED
|
@@ -15,6 +15,8 @@ policy are all present.
|
|
|
15
15
|
| Portability | Node 20/22/24 Linux depth, OS smoke coverage, path/line-ending fixtures, and adapter compatibility evidence. |
|
|
16
16
|
| Observability | `inspect`/`status`/`validate-protocol` shared derived state classification, real schema health, shared evidence, rich doctor findings, receipts, and no telemetry. |
|
|
17
17
|
| Completion enforcement | Canonical contract, persisted route, guide-declared gates, preflight, phase ledger, structured checks, evidence coverage, `audit`, `report`, and `complete` validators. |
|
|
18
|
+
| Agent lifecycle navigation | Read-only `forgeloop next` decisions, stable action/reason output, persisted-state safety, and adapter guidance at lifecycle boundaries. |
|
|
19
|
+
| Execution → Verification handoff | Legal `EXECUTING` → `VERIFYING` transition, implementation-step reconciliation, and preservation of verification evidence. |
|
|
18
20
|
| Pre-contract autonomy | Blocking vs Non-Blocking Decisions policy, explicit ASSUMPTION / source=agent-default recording, safe-boundary invariant, positive and negative tests, and a blind run showing non-blocking ambiguity continues while blocking ambiguity remains user-gated. |
|
|
19
21
|
| Resume/checkpoint | Atomic local state, contract/HEAD/artifact freshness, age warning, schema/secret validation, status, safe validation, and bounded clearing without persisting derived freshness fields. |
|
|
20
22
|
| Multi-agent coordination | Self-contained briefs, write/write and write/read ownership checks, dependency-set validation, reviewer independence, normalized results, and inline fallback. |
|
|
@@ -35,6 +37,16 @@ policy are all present.
|
|
|
35
37
|
- Literal graph runtime and runtime multi-agent orchestration are `N/A by
|
|
36
38
|
design`; compatible harnesses own those capabilities.
|
|
37
39
|
|
|
40
|
+
## Third-run conformance position
|
|
41
|
+
|
|
42
|
+
| Dimension | Classification |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| Execution → Verification | REPRODUCED FAILURE before this fix |
|
|
45
|
+
| Verification serialization | NOT_REACHED |
|
|
46
|
+
| Review transition | NOT_REACHED |
|
|
47
|
+
| Receipt generation | NOT_REACHED |
|
|
48
|
+
| Full conformance | PARTIAL |
|
|
49
|
+
|
|
38
50
|
## Evidence matrix
|
|
39
51
|
|
|
40
52
|
The score is evidence-backed only when the contract and its executable proof
|
package/README.md
CHANGED
|
@@ -137,6 +137,8 @@ Protocol-support commands are local and do not invoke an agent or model:
|
|
|
137
137
|
npx @cassiomc1/forgeloop route --work complete-website --surface ui --risk untrusted-input
|
|
138
138
|
npx @cassiomc1/forgeloop activate
|
|
139
139
|
npx @cassiomc1/forgeloop preflight --json
|
|
140
|
+
npx @cassiomc1/forgeloop next
|
|
141
|
+
npx @cassiomc1/forgeloop next --json
|
|
140
142
|
npx @cassiomc1/forgeloop advance --to EXECUTING
|
|
141
143
|
npx @cassiomc1/forgeloop advance --to VERIFYING
|
|
142
144
|
npx @cassiomc1/forgeloop prepare-completion --json
|
|
@@ -155,6 +157,25 @@ npx @cassiomc1/forgeloop validate-receipt --file ./execution-receipt.json --json
|
|
|
155
157
|
npx @cassiomc1/forgeloop validate-protocol --route-file ./routing-result.json --state-file .forgeloop/work-state.json --receipt-file ./execution-receipt.json --contract-file .forgeloop/current-contract.json --json
|
|
156
158
|
```
|
|
157
159
|
|
|
160
|
+
The query-driven post-implementation path is:
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
implementation
|
|
164
|
+
→ forgeloop next
|
|
165
|
+
→ advance --to VERIFYING
|
|
166
|
+
→ forgeloop next
|
|
167
|
+
→ prepare-completion
|
|
168
|
+
→ forgeloop next
|
|
169
|
+
→ checks + record-check
|
|
170
|
+
→ forgeloop next
|
|
171
|
+
→ advance --to REVIEWING
|
|
172
|
+
→ forgeloop next
|
|
173
|
+
→ complete
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`forgeloop next` and `forgeloop next --json` read persisted state only. They do
|
|
177
|
+
not run project checks or mutate protocol artifacts.
|
|
178
|
+
|
|
158
179
|
`route` expands declared signals into deterministic guide IDs and reason codes.
|
|
159
180
|
`activate` records a session marker without storing prompts or hidden reasoning.
|
|
160
181
|
Before implementation, write the canonical contract, persist the route, create
|
package/conformance/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Run a scenario in a disposable target using the Standard profile first:
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npx @cassiomc1/forgeloop preflight --json
|
|
11
|
+
npx @cassiomc1/forgeloop next --json
|
|
11
12
|
npx @cassiomc1/forgeloop audit --json
|
|
12
13
|
npx @cassiomc1/forgeloop complete --json
|
|
13
14
|
```
|
|
@@ -15,12 +16,16 @@ npx @cassiomc1/forgeloop complete --json
|
|
|
15
16
|
The expected post-implementation path is:
|
|
16
17
|
|
|
17
18
|
```text
|
|
18
|
-
|
|
19
|
+
implementation
|
|
20
|
+
→ forgeloop next
|
|
19
21
|
→ advance --to VERIFYING
|
|
22
|
+
→ forgeloop next
|
|
20
23
|
→ prepare-completion
|
|
21
|
-
→
|
|
24
|
+
→ forgeloop next
|
|
25
|
+
→ checks + record-check
|
|
26
|
+
→ forgeloop next
|
|
22
27
|
→ advance --to REVIEWING
|
|
23
|
-
→
|
|
28
|
+
→ forgeloop next
|
|
24
29
|
→ complete
|
|
25
30
|
```
|
|
26
31
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -24,12 +24,13 @@ import { formatPolicyResult, runPolicy } from "./commands/policy.js";
|
|
|
24
24
|
import { formatBundleResult, runBundle } from "./commands/bundle.js";
|
|
25
25
|
import { formatPrepareCompletionResult, runPrepareCompletion } from "./commands/prepare-completion.js";
|
|
26
26
|
import { formatRecordCheckResult, runRecordCheck } from "./commands/record-check.js";
|
|
27
|
+
import { formatNextActionResult, runNext } from "./commands/next.js";
|
|
27
28
|
import { resolveTarget } from "./core/filesystem.js";
|
|
28
29
|
import { getPackageRoot } from "./core/templates.js";
|
|
29
30
|
import { ARTIFACT_PATHS } from "./core/artifacts.js";
|
|
30
31
|
|
|
31
32
|
function usage(command = null) {
|
|
32
|
-
const commands = "init|doctor|update|activate|route|preflight|advance|prepare-completion|record-check|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
|
|
33
|
+
const commands = "init|doctor|update|activate|route|preflight|advance|next|prepare-completion|record-check|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
|
|
33
34
|
const options = [" --path <directory> target project directory (default: current directory)"];
|
|
34
35
|
if (!command || command === "init" || command === "update") {
|
|
35
36
|
options.push(" --dry-run show planned writes without changing files");
|
|
@@ -51,7 +52,7 @@ function usage(command = null) {
|
|
|
51
52
|
if (!command || command === "advance") {
|
|
52
53
|
options.push(" --to <phase> destination workflow phase");
|
|
53
54
|
}
|
|
54
|
-
if (!command || ["activate", "advance", "prepare-completion", "record-check", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
|
|
55
|
+
if (!command || ["activate", "advance", "next", "prepare-completion", "record-check", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
|
|
55
56
|
options.push(" --json emit structured output as JSON");
|
|
56
57
|
}
|
|
57
58
|
if (!command || ["preflight", "complete", "audit", "report"].includes(command)) {
|
|
@@ -132,7 +133,7 @@ export function parseArgs(argv) {
|
|
|
132
133
|
|
|
133
134
|
for (let index = 0; index < argv.length; index += 1) {
|
|
134
135
|
const argument = argv[index];
|
|
135
|
-
if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "prepare-completion", "record-check", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
|
|
136
|
+
if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "next", "prepare-completion", "record-check", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
|
|
136
137
|
if (command) throw new Error(`Multiple commands are not supported: ${argument}`);
|
|
137
138
|
command = argument;
|
|
138
139
|
} else if (argument === "--help" || argument === "-h") {
|
|
@@ -214,6 +215,10 @@ export function parseArgs(argv) {
|
|
|
214
215
|
if (!id || id.startsWith("-")) throw new Error("--id requires a check ID");
|
|
215
216
|
options.checkId = id;
|
|
216
217
|
index += 1;
|
|
218
|
+
} else if (argument.startsWith("--id=")) {
|
|
219
|
+
const id = argument.slice("--id=".length);
|
|
220
|
+
if (!id || id.startsWith("-")) throw new Error("--id requires a check ID");
|
|
221
|
+
options.checkId = id;
|
|
217
222
|
} else if (argument === "--kind") {
|
|
218
223
|
const kind = argv[index + 1];
|
|
219
224
|
if (!kind || kind.startsWith("-")) throw new Error("--kind requires a check kind");
|
|
@@ -224,6 +229,10 @@ export function parseArgs(argv) {
|
|
|
224
229
|
if (!requirement || requirement.startsWith("-")) throw new Error("--requirement requires an evidence target");
|
|
225
230
|
options.checkRequirement = requirement;
|
|
226
231
|
index += 1;
|
|
232
|
+
} else if (argument.startsWith("--requirement=")) {
|
|
233
|
+
const requirement = argument.slice("--requirement=".length);
|
|
234
|
+
if (!requirement) throw new Error("--requirement requires an evidence target");
|
|
235
|
+
options.checkRequirement = requirement;
|
|
227
236
|
} else if (argument === "--status") {
|
|
228
237
|
const status = argv[index + 1];
|
|
229
238
|
if (!status || status.startsWith("-")) throw new Error("--status requires a check status");
|
|
@@ -244,6 +253,10 @@ export function parseArgs(argv) {
|
|
|
244
253
|
if (!resultText || resultText.startsWith("-")) throw new Error("--result requires recorded text");
|
|
245
254
|
options.checkResult = resultText;
|
|
246
255
|
index += 1;
|
|
256
|
+
} else if (argument.startsWith("--result=")) {
|
|
257
|
+
const resultText = argument.slice("--result=".length);
|
|
258
|
+
if (!resultText) throw new Error("--result requires recorded text");
|
|
259
|
+
options.checkResult = resultText;
|
|
247
260
|
} else if (argument === "--exit-code") {
|
|
248
261
|
const exitCode = argv[index + 1];
|
|
249
262
|
if (!exitCode || exitCode.startsWith("-")) throw new Error("--exit-code requires a non-negative integer");
|
|
@@ -282,7 +295,7 @@ export function parseArgs(argv) {
|
|
|
282
295
|
|
|
283
296
|
if (!command) return { command: null, options };
|
|
284
297
|
|
|
285
|
-
const jsonCommands = ["doctor", "route", "activate", "advance", "prepare-completion", "record-check", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
|
|
298
|
+
const jsonCommands = ["doctor", "route", "activate", "advance", "next", "prepare-completion", "record-check", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
|
|
286
299
|
if (!jsonCommands.includes(command) && options.json) {
|
|
287
300
|
throw new Error(`Option --json is not valid for ${command}`);
|
|
288
301
|
}
|
|
@@ -436,6 +449,12 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
436
449
|
return 0;
|
|
437
450
|
}
|
|
438
451
|
|
|
452
|
+
if (command === "next") {
|
|
453
|
+
const result = await runNext({ target, packageRoot });
|
|
454
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatNextActionResult(result));
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
|
|
439
458
|
if (command === "prepare-completion") {
|
|
440
459
|
const result = await runPrepareCompletion({ target, packageRoot });
|
|
441
460
|
console.log(options.json ? JSON.stringify(result, null, 2) : formatPrepareCompletionResult(result));
|
|
@@ -451,8 +470,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
451
470
|
requirement: options.checkRequirement,
|
|
452
471
|
status: options.checkStatus,
|
|
453
472
|
evidenceKind: options.checkEvidenceKind,
|
|
454
|
-
command: options.checkCommand,
|
|
455
|
-
result: options.checkResult,
|
|
473
|
+
command: options.checkCommand ?? undefined,
|
|
474
|
+
result: options.checkResult ?? undefined,
|
|
456
475
|
exitCode: options.checkExitCode,
|
|
457
476
|
details: options.checkDetails ?? undefined,
|
|
458
477
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getNextAction } from "../core/next-action.js";
|
|
2
|
+
|
|
3
|
+
export async function runNext({ target, packageRoot }) {
|
|
4
|
+
return getNextAction({ target, packageRoot });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatNextActionResult(result) {
|
|
8
|
+
const lines = [
|
|
9
|
+
`FORGELOOP NEXT: ${result.nextAction}`,
|
|
10
|
+
`PHASE: ${result.currentPhase}`,
|
|
11
|
+
];
|
|
12
|
+
if (result.reasons.length > 0) {
|
|
13
|
+
lines.push("REASONS:");
|
|
14
|
+
lines.push(...result.reasons.map((reason) => `- ${reason.code}: ${reason.message}`));
|
|
15
|
+
}
|
|
16
|
+
if (result.commands.length > 0) {
|
|
17
|
+
lines.push("COMMANDS (SAFE SYNOPSIS ONLY):");
|
|
18
|
+
lines.push(...result.commands.map((command) => `- ${command}`));
|
|
19
|
+
}
|
|
20
|
+
if (result.commandSpecs.length > 0) {
|
|
21
|
+
lines.push("STRUCTURED COMMAND SPECS: Available in --json output; direct-process argv data, not shell syntax.");
|
|
22
|
+
}
|
|
23
|
+
if (result.missingArtifacts.length > 0) {
|
|
24
|
+
lines.push("MISSING ARTIFACTS:");
|
|
25
|
+
lines.push(...result.missingArtifacts.map((artifact) => `- ${artifact}`));
|
|
26
|
+
}
|
|
27
|
+
if (result.terminal) lines.push("STATE: TERMINAL");
|
|
28
|
+
return `${lines.join("\n")}\n`;
|
|
29
|
+
}
|
|
@@ -5,16 +5,18 @@ import {
|
|
|
5
5
|
writeJsonArtifact,
|
|
6
6
|
} from "./artifacts.js";
|
|
7
7
|
import { readContract } from "./contract.js";
|
|
8
|
-
import { appendProtocolEvent } from "./events.js";
|
|
8
|
+
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
9
9
|
import { completionEvidenceForGuides } from "./guide-metadata.js";
|
|
10
10
|
import { createCheck } from "./checks.js";
|
|
11
11
|
import { createEvidence } from "./evidence.js";
|
|
12
12
|
import { coverageForRequirements } from "./coverage.js";
|
|
13
|
+
import { assertCompletionRelationships } from "./completion-relationships.js";
|
|
13
14
|
import { evaluatePreflight } from "./preflight.js";
|
|
14
15
|
import { currentChangedPaths } from "./repository.js";
|
|
15
16
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
16
17
|
import { createReceipt, validateReceipt } from "./receipt.js";
|
|
17
18
|
import { readWorkState, writeWorkState } from "./work-state.js";
|
|
19
|
+
import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
|
|
18
20
|
|
|
19
21
|
function artifactError(code, message, artifacts = []) {
|
|
20
22
|
const error = new Error(message);
|
|
@@ -72,6 +74,9 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
72
74
|
if (!state) {
|
|
73
75
|
throw artifactError("E_STATE_MISSING", "Work state is required before preparing completion", [ARTIFACT_PATHS.state]);
|
|
74
76
|
}
|
|
77
|
+
if (hasExecutionStarted(state.phase)) {
|
|
78
|
+
await assertExecutionPrerequisites({ target, state, packageRoot });
|
|
79
|
+
}
|
|
75
80
|
|
|
76
81
|
let existing = null;
|
|
77
82
|
try {
|
|
@@ -90,18 +95,25 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
90
95
|
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
91
96
|
});
|
|
92
97
|
const existingValue = existing?.value ?? {};
|
|
98
|
+
assertCompletionRelationships({
|
|
99
|
+
contract,
|
|
100
|
+
route,
|
|
101
|
+
state,
|
|
102
|
+
receipt: existingValue.taskId ? existingValue : null,
|
|
103
|
+
requiredEvidence,
|
|
104
|
+
requireRequiredChecks: false,
|
|
105
|
+
});
|
|
93
106
|
const changedPaths = existing
|
|
94
107
|
? [...(existingValue.changedPaths ?? [])]
|
|
95
108
|
: (await currentChangedPaths(target) ?? []);
|
|
96
|
-
const checks = [...
|
|
109
|
+
const checks = existing ? [...existingValue.checks] : [...state.checks];
|
|
110
|
+
const evidence = existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
|
|
97
111
|
const receipt = await createReceipt({
|
|
98
112
|
...existingValue,
|
|
99
113
|
taskId: contract.value.taskId,
|
|
100
114
|
contractFingerprint: contract.fingerprint,
|
|
101
115
|
routeFingerprint: route.fingerprint,
|
|
102
|
-
|
|
103
|
-
? { stateFingerprint: canonicalFingerprint(state) }
|
|
104
|
-
: {}),
|
|
116
|
+
stateFingerprint: canonicalFingerprint(state),
|
|
105
117
|
status: existingValue.status ?? "in-progress",
|
|
106
118
|
taskStatus: existingValue.taskStatus ?? "in-progress",
|
|
107
119
|
verificationStatus: existingValue.verificationStatus ?? "not-verified",
|
|
@@ -110,7 +122,7 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
110
122
|
selectedGuides: [...route.value.guides],
|
|
111
123
|
changedPaths,
|
|
112
124
|
checks,
|
|
113
|
-
evidence
|
|
125
|
+
evidence,
|
|
114
126
|
evidenceCoverage: coverageForRequirements(requiredEvidence, checks),
|
|
115
127
|
review: existingValue.review ?? { status: "not-run", independent: false },
|
|
116
128
|
limitations: [...(existingValue.limitations ?? [])],
|
|
@@ -196,6 +208,7 @@ export async function recordCheck({
|
|
|
196
208
|
[ARTIFACT_PATHS.state],
|
|
197
209
|
);
|
|
198
210
|
}
|
|
211
|
+
await assertExecutionPrerequisites({ target, state, packageRoot });
|
|
199
212
|
|
|
200
213
|
const existingReceipt = await readCurrentReceipt(target, packageRoot);
|
|
201
214
|
await validateReceipt(existingReceipt.value, packageRoot);
|
|
@@ -234,6 +247,26 @@ export async function recordCheck({
|
|
|
234
247
|
packageRoot,
|
|
235
248
|
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
236
249
|
});
|
|
250
|
+
assertCompletionRelationships({
|
|
251
|
+
contract,
|
|
252
|
+
route,
|
|
253
|
+
state,
|
|
254
|
+
receipt: existingReceipt.value,
|
|
255
|
+
requiredEvidence,
|
|
256
|
+
requireRequiredChecks: false,
|
|
257
|
+
});
|
|
258
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
259
|
+
if (!ledger.valid) {
|
|
260
|
+
const first = ledger.errors[0];
|
|
261
|
+
throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
|
|
262
|
+
}
|
|
263
|
+
if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
|
|
264
|
+
throw artifactError(
|
|
265
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
266
|
+
"record-check requires VERIFICATION_STARTED in the current task ledger",
|
|
267
|
+
[ARTIFACT_PATHS.events],
|
|
268
|
+
);
|
|
269
|
+
}
|
|
237
270
|
const coverage = coverageForRequirements(requiredEvidence, checks);
|
|
238
271
|
const nextState = {
|
|
239
272
|
...state,
|
|
@@ -247,11 +280,18 @@ export async function recordCheck({
|
|
|
247
280
|
checks,
|
|
248
281
|
evidence: evidenceList,
|
|
249
282
|
evidenceCoverage: coverage,
|
|
250
|
-
|
|
251
|
-
? { stateFingerprint: canonicalFingerprint(nextState) }
|
|
252
|
-
: {}),
|
|
283
|
+
stateFingerprint: canonicalFingerprint(nextState),
|
|
253
284
|
}, packageRoot);
|
|
254
285
|
|
|
286
|
+
assertCompletionRelationships({
|
|
287
|
+
contract,
|
|
288
|
+
route,
|
|
289
|
+
state: nextState,
|
|
290
|
+
receipt: nextReceipt,
|
|
291
|
+
requiredEvidence,
|
|
292
|
+
requireRequiredChecks: false,
|
|
293
|
+
});
|
|
294
|
+
|
|
255
295
|
await writeWorkState(target, nextState, { packageRoot });
|
|
256
296
|
const written = await writeJsonArtifact(
|
|
257
297
|
target,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { ARTIFACT_PATHS, canonicalFingerprint } from "./artifacts.js";
|
|
2
|
+
import { assertCheckList, requiredChecksSatisfied } from "./checks.js";
|
|
3
|
+
import { assertCoverageList, coverageForRequirements } from "./coverage.js";
|
|
4
|
+
import { assertEvidenceList } from "./evidence.js";
|
|
5
|
+
|
|
6
|
+
function issue(code, message, artifacts = []) {
|
|
7
|
+
return { code, message, artifacts };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sameValue(left, right) {
|
|
11
|
+
return canonicalFingerprint(left) === canonicalFingerprint(right);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function addAssertion(errors, assertion, code, artifacts) {
|
|
15
|
+
try {
|
|
16
|
+
assertion();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
errors.push(issue(code, error.message, artifacts));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function stateIdentityErrors({
|
|
23
|
+
contract,
|
|
24
|
+
route,
|
|
25
|
+
state,
|
|
26
|
+
} = {}) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
const contractValue = contract?.value ?? contract;
|
|
29
|
+
const contractFingerprint = contract?.fingerprint;
|
|
30
|
+
if (contractValue && state && contractValue.taskId !== state.taskId) {
|
|
31
|
+
errors.push(issue("E_STATE_TASK_MISMATCH", "Work state does not belong to the current contract task", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.state]));
|
|
32
|
+
}
|
|
33
|
+
if (contractFingerprint && state && state.contractFingerprint !== contractFingerprint) {
|
|
34
|
+
errors.push(issue("E_CONTRACT_STALE", "Work state does not match the current contract", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.state]));
|
|
35
|
+
}
|
|
36
|
+
if (contractFingerprint && route && route.value.contractFingerprint !== contractFingerprint) {
|
|
37
|
+
errors.push(issue("E_ROUTE_STALE", "Routing result does not match the current contract", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route]));
|
|
38
|
+
}
|
|
39
|
+
if (route && state && route.value.contractFingerprint !== undefined
|
|
40
|
+
&& route.value.contractFingerprint !== state.contractFingerprint) {
|
|
41
|
+
errors.push(issue("E_ROUTE_STALE", "Routing result does not match the work state contract", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.state]));
|
|
42
|
+
}
|
|
43
|
+
if (route && state && (state.routeFingerprint !== route.fingerprint || !sameValue(state.selectedGuides, route.value.guides))) {
|
|
44
|
+
errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "Work state does not match the persisted route identity", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.state]));
|
|
45
|
+
}
|
|
46
|
+
return errors;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function assertStateIdentity(input) {
|
|
50
|
+
const errors = stateIdentityErrors(input);
|
|
51
|
+
if (errors.length === 0) return;
|
|
52
|
+
const first = errors[0];
|
|
53
|
+
const error = new Error(first.message);
|
|
54
|
+
error.code = first.code;
|
|
55
|
+
error.artifacts = first.artifacts;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function completionRelationshipErrors({
|
|
60
|
+
contract,
|
|
61
|
+
route,
|
|
62
|
+
state,
|
|
63
|
+
receipt,
|
|
64
|
+
requiredEvidence = [],
|
|
65
|
+
requireReceiptStateFingerprint = true,
|
|
66
|
+
requireRequiredChecks = true,
|
|
67
|
+
} = {}) {
|
|
68
|
+
const errors = stateIdentityErrors({ contract, route, state });
|
|
69
|
+
const contractValue = contract?.value ?? contract;
|
|
70
|
+
const contractFingerprint = contract?.fingerprint;
|
|
71
|
+
if (contractValue && receipt && contractValue.taskId !== receipt.taskId) {
|
|
72
|
+
errors.push(issue("E_RECEIPT_TASK_MISMATCH", "Execution receipt does not belong to the current contract task", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt]));
|
|
73
|
+
}
|
|
74
|
+
if (route && receipt && (receipt.routeFingerprint !== route.fingerprint || !sameValue(receipt.selectedGuides, route.value.guides))) {
|
|
75
|
+
errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "Execution receipt does not match the persisted route identity", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.receipt]));
|
|
76
|
+
}
|
|
77
|
+
if (contractFingerprint && receipt && receipt.contractFingerprint !== contractFingerprint) {
|
|
78
|
+
errors.push(issue("E_RECEIPT_CONTRACT_MISMATCH", "Execution receipt does not match the current contract", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt]));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (state) {
|
|
82
|
+
addAssertion(errors, () => assertCheckList(state.checks, "work-state.checks"), "E_CHECK_INVALID", [ARTIFACT_PATHS.state]);
|
|
83
|
+
addAssertion(errors, () => assertEvidenceList(state.verificationEvidence, "work-state.verificationEvidence"), "E_EVIDENCE_INVALID", [ARTIFACT_PATHS.state]);
|
|
84
|
+
}
|
|
85
|
+
if (receipt) {
|
|
86
|
+
addAssertion(errors, () => assertCheckList(receipt.checks, "receipt.checks"), "E_CHECK_INVALID", [ARTIFACT_PATHS.receipt]);
|
|
87
|
+
addAssertion(errors, () => assertEvidenceList(receipt.evidence ?? [], "receipt.evidence"), "E_EVIDENCE_INVALID", [ARTIFACT_PATHS.receipt]);
|
|
88
|
+
if (requireRequiredChecks) {
|
|
89
|
+
for (const error of requiredChecksSatisfied(receipt.checks, requiredEvidence)) {
|
|
90
|
+
errors.push(issue(error.code, error.message, [ARTIFACT_PATHS.receipt]));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const expectedCoverage = state ? coverageForRequirements(requiredEvidence, state.checks) : [];
|
|
96
|
+
if (state?.evidenceCoverage !== undefined) {
|
|
97
|
+
addAssertion(errors, () => assertCoverageList(state.evidenceCoverage, "work-state.evidenceCoverage"), "E_EVIDENCE_COVERAGE_INVALID", [ARTIFACT_PATHS.state]);
|
|
98
|
+
if (!sameValue(state.evidenceCoverage, expectedCoverage)) {
|
|
99
|
+
errors.push(issue("E_EVIDENCE_COVERAGE_INVALID", "Work state evidence coverage does not match its checks", [ARTIFACT_PATHS.state]));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (receipt) {
|
|
103
|
+
if (receipt.evidenceCoverage === undefined) {
|
|
104
|
+
errors.push(issue("E_EVIDENCE_COVERAGE_INVALID", "Execution receipt requires evidence coverage", [ARTIFACT_PATHS.receipt]));
|
|
105
|
+
} else {
|
|
106
|
+
addAssertion(errors, () => assertCoverageList(receipt.evidenceCoverage, "receipt.evidenceCoverage"), "E_EVIDENCE_COVERAGE_INVALID", [ARTIFACT_PATHS.receipt]);
|
|
107
|
+
if (!sameValue(receipt.evidenceCoverage, expectedCoverage)) {
|
|
108
|
+
errors.push(issue("E_EVIDENCE_COVERAGE_INVALID", "Execution receipt evidence coverage does not match current checks", [ARTIFACT_PATHS.receipt]));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (state && receipt) {
|
|
114
|
+
if (requireReceiptStateFingerprint && receipt.stateFingerprint === undefined) {
|
|
115
|
+
errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
|
|
116
|
+
} else if (receipt.stateFingerprint !== undefined && receipt.stateFingerprint !== canonicalFingerprint(state)) {
|
|
117
|
+
errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Execution receipt does not match the recorded work state", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
|
|
118
|
+
}
|
|
119
|
+
if (!sameValue(state.checks, receipt.checks)) {
|
|
120
|
+
errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Work state and execution receipt checks diverge", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
|
|
121
|
+
}
|
|
122
|
+
if (!sameValue(state.verificationEvidence, receipt.evidence ?? [])) {
|
|
123
|
+
errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Work state and execution receipt evidence diverge", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
|
|
124
|
+
}
|
|
125
|
+
if (state.evidenceCoverage !== undefined && receipt.evidenceCoverage !== undefined
|
|
126
|
+
&& !sameValue(state.evidenceCoverage, receipt.evidenceCoverage)) {
|
|
127
|
+
errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Work state and execution receipt coverage diverge", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return errors;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function assertCompletionRelationships(input) {
|
|
134
|
+
const errors = completionRelationshipErrors(input);
|
|
135
|
+
if (errors.length === 0) return;
|
|
136
|
+
const first = errors[0];
|
|
137
|
+
const error = new Error(first.message);
|
|
138
|
+
error.code = first.code;
|
|
139
|
+
error.artifacts = first.artifacts;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|