@cassiomc1/forgeloop 1.1.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.cursor/rules/project-loop.mdc +1 -1
  2. package/.github/copilot-instructions.md +1 -1
  3. package/AGENTS.md +1 -1
  4. package/CLAUDE.md +1 -1
  5. package/DOCS_INDEX.md +3 -0
  6. package/ENG/design-code-eng.md +124 -0
  7. package/ENG/premium-sites-studio-eng.md +28 -0
  8. package/ENG/taste-frontend-eng.md +3 -2
  9. package/ENG/test-code-eng.md +45 -0
  10. package/LOOP_ENGINEERING.md +74 -0
  11. package/LOOP_SYSTEM_DESIGN.md +9 -5
  12. package/ORCHESTRATOR_INTEGRATION.md +41 -6
  13. package/PROTOCOL_INTEGRATION.md +13 -0
  14. package/README.md +40 -6
  15. package/TERMINOLOGY.md +10 -0
  16. package/THIRD_PARTY_NOTICES.md +58 -1
  17. package/THREAT_MODEL.md +12 -1
  18. package/docs/ARTIFACT_REFERENCE.md +152 -2
  19. package/docs/CLI_REFERENCE.md +346 -30
  20. package/docs/CROSS_HARNESS_CONTINUITY.md +1 -0
  21. package/docs/DOCUMENTATION_GUIDE.md +41 -4
  22. package/docs/GETTING_STARTED.md +39 -8
  23. package/docs/RECIPES.md +66 -7
  24. package/docs/TROUBLESHOOTING.md +279 -6
  25. package/package.json +1 -1
  26. package/schemas/policy-baseline.schema.json +26 -0
  27. package/schemas/policy-discovery.schema.json +45 -0
  28. package/schemas/policy-lock.schema.json +16 -0
  29. package/schemas/policy-rules.schema.json +48 -0
  30. package/schemas/policy-snapshot.schema.json +16 -0
  31. package/src/cli.js +102 -1
  32. package/src/commands/baseline.js +120 -0
  33. package/src/commands/init.js +304 -6
  34. package/src/commands/next.js +15 -1
  35. package/src/commands/policy-diff.js +51 -0
  36. package/src/commands/policy-discover.js +42 -0
  37. package/src/commands/policy-status.js +33 -0
  38. package/src/commands/profile-interview.js +50 -0
  39. package/src/commands/progress.js +51 -0
  40. package/src/commands/reconcile-closure.js +49 -0
  41. package/src/commands/record-decision-criterion.js +34 -0
  42. package/src/commands/record-diagnosis.js +49 -0
  43. package/src/commands/rule-verify.js +36 -0
  44. package/src/commands/validate-receipt.js +38 -3
  45. package/src/core/artifact-registry.js +60 -0
  46. package/src/core/audit.js +24 -0
  47. package/src/core/cli-command-definitions.js +163 -7
  48. package/src/core/cli-metadata.js +1 -1
  49. package/src/core/completion-artifacts.js +29 -3
  50. package/src/core/completion.js +101 -10
  51. package/src/core/diagnosis-model.js +214 -0
  52. package/src/core/diagnosis.js +171 -0
  53. package/src/core/error-codes.js +292 -0
  54. package/src/core/events.js +47 -1
  55. package/src/core/execution-prerequisites.js +38 -20
  56. package/src/core/execution.js +20 -3
  57. package/src/core/native-adapters.js +14 -4
  58. package/src/core/next-action-model.js +40 -5
  59. package/src/core/next-action.js +234 -91
  60. package/src/core/phase.js +29 -0
  61. package/src/core/policy-adapters.js +276 -0
  62. package/src/core/policy-baseline.js +144 -0
  63. package/src/core/policy-diff.js +133 -0
  64. package/src/core/policy-discovery.js +225 -0
  65. package/src/core/policy-engine.js +533 -0
  66. package/src/core/policy-mutation.js +139 -0
  67. package/src/core/preflight-consistency.js +23 -15
  68. package/src/core/preflight-model.js +10 -2
  69. package/src/core/preflight.js +65 -1
  70. package/src/core/progress.js +143 -0
  71. package/src/core/protocol.js +8 -0
  72. package/src/core/reconcile-closure.js +173 -0
  73. package/src/core/schema-validation.js +6 -0
  74. package/src/core/settlement-model.js +85 -0
  75. package/src/core/settlement.js +78 -0
  76. package/src/core/task-context.js +11 -0
  77. package/src/core/task-discovery.js +67 -1
  78. package/src/core/task-paths.js +9 -0
  79. package/src/core/templates.js +5 -0
@@ -86,64 +86,72 @@ export async function validateReadyProtocolConsistency({
86
86
  persisted,
87
87
  current = null,
88
88
  evaluateCurrentPreflight,
89
+ taskId = null,
89
90
  } = {}) {
90
91
  if (persisted?.status !== "READY") return [];
91
- const result = current ?? await evaluateCurrentPreflight({ target, packageRoot });
92
+ const result = current ?? await evaluateCurrentPreflight({ target, packageRoot, taskId });
92
93
  const errors = [...validatePersistedPreflight(persisted, result)];
93
94
 
95
+ const stateRel = taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state;
96
+ const eventsRel = taskId ? taskArtifactPath(taskId, "events") : ARTIFACT_PATHS.events;
97
+ const preflightRel = taskId ? taskArtifactPath(taskId, "preflight") : ARTIFACT_PATHS.preflight;
98
+ const contractRel = taskId ? taskArtifactPath(taskId, "contract") : ARTIFACT_PATHS.contract;
99
+ const routeRel = taskId ? taskArtifactPath(taskId, "route") : ARTIFACT_PATHS.route;
100
+ const gatesRel = taskId ? taskArtifactPath(taskId, "gates") : ARTIFACT_PATHS.gates;
101
+
94
102
  let state = null;
95
103
  try {
96
- state = await readWorkState(target, packageRoot);
104
+ state = await readWorkState(target, { packageRoot, taskId });
97
105
  } catch (error) {
98
- errors.push(issue("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]));
106
+ errors.push(issue("E_STATE_INVALID", error.message, [stateRel]));
99
107
  }
100
108
  if (!state) {
101
109
  errors.push(issue(
102
110
  "E_STATE_MISSING_AFTER_PREFLIGHT_READY",
103
111
  "A persisted READY preflight must have a resumable work-state checkpoint",
104
- [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.state],
112
+ [preflightRel, stateRel],
105
113
  ));
106
114
  } else {
107
115
  if (state.taskId !== persisted.taskId) {
108
- errors.push(issue("E_STATE_TASK_MISMATCH", "The resumable checkpoint does not belong to the READY preflight task", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight]));
116
+ errors.push(issue("E_STATE_TASK_MISMATCH", "The resumable checkpoint does not belong to the READY preflight task", [stateRel, preflightRel]));
109
117
  }
110
118
  if (state.contractFingerprint !== result.fingerprints.contract) {
111
- errors.push(issue("E_CONTRACT_STALE", "The resumable checkpoint does not match the READY contract fingerprint", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract]));
119
+ errors.push(issue("E_CONTRACT_STALE", "The resumable checkpoint does not match the READY contract fingerprint", [stateRel, contractRel]));
112
120
  }
113
121
  if (state.routeFingerprint !== result.fingerprints.routing) {
114
- errors.push(issue("E_ROUTE_STALE", "The resumable checkpoint does not match the READY routing fingerprint", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.route]));
122
+ errors.push(issue("E_ROUTE_STALE", "The resumable checkpoint does not match the READY routing fingerprint", [stateRel, routeRel]));
115
123
  }
116
124
  if (JSON.stringify(state.selectedGuides) !== JSON.stringify(result.routing.guides)) {
117
- errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "The resumable checkpoint guides do not match the READY routing result", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.route]));
125
+ errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "The resumable checkpoint guides do not match the READY routing result", [stateRel, routeRel]));
118
126
  }
119
127
  if (!sameStringSet(state.requiredGates ?? [], persisted.requiredGates)
120
128
  || !sameStringSet(state.satisfiedGates ?? [], persisted.satisfiedGates)) {
121
- errors.push(issue("E_PREFLIGHT_GATES_STALE", "The resumable checkpoint gate sets do not match the READY preflight", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.gates]));
129
+ errors.push(issue("E_PREFLIGHT_GATES_STALE", "The resumable checkpoint gate sets do not match the READY preflight", [stateRel, preflightRel, gatesRel]));
122
130
  }
123
131
  }
124
132
 
125
- const ledger = await validateEventLedger(target, packageRoot);
133
+ const ledger = await validateEventLedger(target, packageRoot, { taskId });
126
134
  if (!ledger.valid) {
127
- errors.push(...ledger.errors.map((error) => issue(error.code ?? "E_EVENT_INVALID", error.message, [ARTIFACT_PATHS.events])));
135
+ errors.push(...ledger.errors.map((error) => issue(error.code ?? "E_EVENT_INVALID", error.message, [eventsRel])));
128
136
  }
129
137
  const events = ledger.events ?? [];
130
138
  for (const requiredEvent of ["CONTRACT_VALIDATED", "ROUTE_VALIDATED"]) {
131
139
  if (!events.some((event) => event.event === requiredEvent && event.taskId === persisted.taskId)) {
132
- errors.push(issue("E_PREFLIGHT_EVENT_MISSING", `READY preflight is missing lifecycle event: ${requiredEvent}`, [ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight]));
140
+ errors.push(issue("E_PREFLIGHT_EVENT_MISSING", `READY preflight is missing lifecycle event: ${requiredEvent}`, [eventsRel, preflightRel]));
133
141
  }
134
142
  }
135
143
  for (const gate of persisted.satisfiedGates ?? []) {
136
144
  if (!events.some((event) => event.event === "GATE_SATISFIED"
137
145
  && event.taskId === persisted.taskId
138
146
  && event.details?.gate === gate)) {
139
- errors.push(issue("E_PREFLIGHT_GATE_EVENT_MISSING", `READY preflight is missing lifecycle gate event: ${gate}`, [ARTIFACT_PATHS.events, `${ARTIFACT_PATHS.gates}/${gate}.json`]));
147
+ errors.push(issue("E_PREFLIGHT_GATE_EVENT_MISSING", `READY preflight is missing lifecycle gate event: ${gate}`, [eventsRel, `${gatesRel}/${gate}.json`]));
140
148
  }
141
149
  }
142
150
  const readyEvents = events.filter((event) => event.event === "PREFLIGHT_READY" && event.taskId === persisted.taskId);
143
151
  if (readyEvents.length === 0) {
144
- errors.push(issue("E_PREFLIGHT_READY_EVENT_MISSING", "Persisted READY preflight is missing the matching PREFLIGHT_READY lifecycle event", [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events]));
152
+ errors.push(issue("E_PREFLIGHT_READY_EVENT_MISSING", "Persisted READY preflight is missing the matching PREFLIGHT_READY lifecycle event", [preflightRel, eventsRel]));
145
153
  } else if (!readyEvents.some((event) => sameReadyPreflightEvent(event, result))) {
146
- errors.push(issue("E_PREFLIGHT_READY_EVENT_MISMATCH", "PREFLIGHT_READY lifecycle details do not match the persisted READY preflight", [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events]));
154
+ errors.push(issue("E_PREFLIGHT_READY_EVENT_MISMATCH", "PREFLIGHT_READY lifecycle details do not match the persisted READY preflight", [preflightRel, eventsRel]));
147
155
  }
148
156
  return sortIssues(errors);
149
157
  }
@@ -34,11 +34,19 @@ export function sameStringSet(left, right) {
34
34
  export function validatePersistedPreflight(persisted, current) {
35
35
  const errors = [];
36
36
  if (persisted?.status !== "READY") {
37
- errors.push(issue("E_PREFLIGHT_NOT_READY", "A persisted READY preflight is required", [ARTIFACT_PATHS.preflight]));
37
+ if (Array.isArray(persisted?.errors) && persisted.errors.length > 0) {
38
+ errors.push(...persisted.errors);
39
+ } else {
40
+ errors.push(issue("E_PREFLIGHT_NOT_READY", "A persisted READY preflight is required", [ARTIFACT_PATHS.preflight]));
41
+ }
38
42
  return errors;
39
43
  }
40
44
  if (current?.status !== "READY") {
41
- errors.push(issue("E_PREFLIGHT_NOT_READY", "The current preflight evaluation is not READY", [ARTIFACT_PATHS.preflight]));
45
+ if (Array.isArray(current?.errors) && current.errors.length > 0) {
46
+ errors.push(...current.errors);
47
+ } else {
48
+ errors.push(issue("E_PREFLIGHT_NOT_READY", "The current preflight evaluation is not READY", [ARTIFACT_PATHS.preflight]));
49
+ }
42
50
  }
43
51
  if (persisted.taskId !== current?.taskId) {
44
52
  errors.push(issue(
@@ -27,7 +27,7 @@ import {
27
27
  import { PROFILE_PATH } from "./target-layout.js";
28
28
  import { readPersistedRoute } from "./route-artifact.js";
29
29
  import { validateEventLedger } from "./events.js";
30
- import { taskArtifactPath } from "./task-paths.js";
30
+ import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
31
31
 
32
32
  const PREVIEW_DECISION_LIMIT = 10;
33
33
  const PREVIEW_DECISION_MAX_LENGTH = 240;
@@ -91,6 +91,24 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
91
91
  errors.push(issue("E_CONTRACT_STALE", "work-state references a different contract", [contractRelPath, stateRelPath]));
92
92
  }
93
93
 
94
+ const { detectPolicyCapability } = await import("./policy-engine.js");
95
+ const policyCapability = await detectPolicyCapability(target, packageRoot);
96
+ if (policyCapability === "INVALID") {
97
+ errors.push(issue(
98
+ "E_POLICY_INVALID",
99
+ "Executable policy artifacts are present but invalid.",
100
+ [
101
+ PROJECT_ARTIFACT_PATHS.policyRules,
102
+ PROJECT_ARTIFACT_PATHS.policyBaseline,
103
+ PROJECT_ARTIFACT_PATHS.policyDiscovery,
104
+ PROJECT_ARTIFACT_PATHS.policyLock,
105
+ ],
106
+ {
107
+ next: "Repair the invalid policy artifact and rerun forgeloop preflight.",
108
+ },
109
+ ));
110
+ }
111
+
94
112
  const sortedErrors = sortIssues(errors);
95
113
  const effectiveTaskId = taskId ?? contract?.value?.taskId ?? state?.taskId ?? "unknown";
96
114
  return {
@@ -178,6 +196,52 @@ export async function runPreflight({
178
196
  }
179
197
 
180
198
  if (result.taskId !== "unknown") {
199
+ const {
200
+ detectPolicyCapability,
201
+ loadEffectiveRules,
202
+ readBaseline,
203
+ computePolicyLockData,
204
+ readTaskPolicySnapshot,
205
+ writeTaskPolicySnapshot,
206
+ } = await import("./policy-engine.js");
207
+
208
+ const policyCapability = await detectPolicyCapability(target, packageRoot);
209
+ if (policyCapability === "INVALID") {
210
+ throw preflightError(
211
+ "E_POLICY_INVALID",
212
+ "Executable policy artifacts are present but invalid.",
213
+ [
214
+ PROJECT_ARTIFACT_PATHS.policyRules,
215
+ PROJECT_ARTIFACT_PATHS.policyBaseline,
216
+ PROJECT_ARTIFACT_PATHS.policyDiscovery,
217
+ PROJECT_ARTIFACT_PATHS.policyLock,
218
+ ],
219
+ );
220
+ }
221
+
222
+ if (policyCapability === "AVAILABLE") {
223
+ try {
224
+ const existingSnapshot = await readTaskPolicySnapshot(target, result.taskId, packageRoot);
225
+ if (!existingSnapshot) {
226
+ const rules = await loadEffectiveRules(target, packageRoot);
227
+ const baseline = await readBaseline(target, packageRoot);
228
+ const lock = computePolicyLockData(rules, baseline);
229
+ const snapshot = {
230
+ schemaVersion: 1,
231
+ policyDigest: lock.digest,
232
+ rules,
233
+ baseline: baseline ?? { schemaVersion: 1, entries: [] },
234
+ baselineDigest: lock.baselineDigest,
235
+ capturedAt: new Date().toISOString(),
236
+ };
237
+ await writeTaskPolicySnapshot(target, result.taskId, snapshot, packageRoot);
238
+ }
239
+ } catch (error) {
240
+ const snapRel = taskArtifactPath(result.taskId, "policySnapshot");
241
+ throw preflightError("E_POLICY_SNAPSHOT_WRITE_FAILED", `Failed to persist task policy snapshot: ${error.message}`, [snapRel]);
242
+ }
243
+ }
244
+
181
245
  await appendActivationEvents(target, packageRoot, ledger, result, { eventsPath, taskId });
182
246
  const afterEvents = await validateEventLedger(target, packageRoot, { eventsPath, taskId });
183
247
  if (!afterEvents.valid) {
@@ -0,0 +1,143 @@
1
+ import { diagnosisEventsForTask } from "./diagnosis-model.js";
2
+
3
+ export const PROGRESS_STATUS = Object.freeze({
4
+ ADVANCING: "ADVANCING",
5
+ WATCH: "WATCH",
6
+ STALLED: "STALLED",
7
+ });
8
+
9
+ export const PROGRESS_SIGNAL = Object.freeze({
10
+ NO_DIAGNOSTIC_INFORMATION_GAIN: "NO_DIAGNOSTIC_INFORMATION_GAIN",
11
+ REPEATED_FAILED_REQUIREMENT: "REPEATED_FAILED_REQUIREMENT",
12
+ REPEATED_FAILURE_WITH_SAME_DIAGNOSIS: "REPEATED_FAILURE_WITH_SAME_DIAGNOSIS",
13
+ HIGH_CORRECTION_CYCLE_COUNT: "HIGH_CORRECTION_CYCLE_COUNT",
14
+ });
15
+
16
+ export function diagnosisRequirements(diagnosis, checksById) {
17
+ if (!diagnosis) return [];
18
+ return [...new Set(
19
+ (diagnosis.evidenceRefs ?? [])
20
+ .map((id) => checksById.get(id)?.requirement)
21
+ .filter(Boolean),
22
+ )].sort();
23
+ }
24
+
25
+ export function evaluateProgress({ state, events = [] } = {}) {
26
+ const signals = [];
27
+ let status = PROGRESS_STATUS.ADVANCING;
28
+
29
+ if (!state) {
30
+ return { status, signals };
31
+ }
32
+
33
+ const taskEvents = Array.isArray(events) ? events.filter((e) => !state.taskId || e.taskId === state.taskId) : [];
34
+ const diagEvents = diagnosisEventsForTask(taskEvents, state.taskId);
35
+ const latestDiag = diagEvents.at(-1)?.details ?? null;
36
+
37
+ // Build checksById index for resolving requirement from check IDs
38
+ const checksById = new Map();
39
+ for (const check of state.checks ?? []) {
40
+ if (check.id) checksById.set(check.id, check);
41
+ if (check.checkId) checksById.set(check.checkId, check);
42
+ }
43
+ for (const event of taskEvents) {
44
+ if (event.event === "VERIFICATION_RECORDED" && event.details) {
45
+ if (event.details.id) checksById.set(event.details.id, event.details);
46
+ if (event.details.checkId) checksById.set(event.details.checkId, event.details);
47
+ }
48
+ }
49
+
50
+ // 1. Check if latest diagnosis has NO information gain (global stall)
51
+ if (latestDiag && latestDiag.informationGain === "NONE") {
52
+ status = PROGRESS_STATUS.STALLED;
53
+ signals.push({
54
+ code: PROGRESS_SIGNAL.NO_DIAGNOSTIC_INFORMATION_GAIN,
55
+ severity: "BLOCKING_FOR_RETRY",
56
+ message: "Latest diagnosis repeats the prior hypothesis with the same evidence.",
57
+ verificationCycles: [latestDiag.verificationCycle],
58
+ });
59
+ }
60
+
61
+ // 2. Track failed requirements across distinct verification cycles
62
+ const reqCycles = new Map();
63
+ const checks = state.checks ?? [];
64
+ for (const check of checks) {
65
+ if (check.status === "failed" || check.status === "blocked") {
66
+ const cycle = check.details?.verificationCycle ?? state.verificationCycle ?? 1;
67
+ const req = check.requirement ?? "default";
68
+ if (!reqCycles.has(req)) reqCycles.set(req, new Set());
69
+ reqCycles.get(req).add(cycle);
70
+ }
71
+ }
72
+
73
+ // Also extract from VERIFICATION_RECORDED events
74
+ for (const event of taskEvents) {
75
+ if (event.event === "VERIFICATION_RECORDED") {
76
+ const details = event.details ?? {};
77
+ if (details.status === "failed" || details.status === "blocked") {
78
+ const cycle = details.verificationCycle ?? 1;
79
+ const req = details.requirement ?? "default";
80
+ if (!reqCycles.has(req)) reqCycles.set(req, new Set());
81
+ reqCycles.get(req).add(cycle);
82
+ }
83
+ }
84
+ }
85
+
86
+ const latestDiagReqs = diagnosisRequirements(latestDiag, checksById);
87
+
88
+ for (const [req, cyclesSet] of [...reqCycles.entries()].sort(([a], [b]) => a.localeCompare(b))) {
89
+ if (cyclesSet.size >= 3) {
90
+ const sortedCycles = [...cyclesSet].sort((a, b) => a - b);
91
+ const isLatestStalledForThisReq = latestDiag && latestDiag.informationGain === "NONE" && latestDiagReqs.includes(req);
92
+ if (isLatestStalledForThisReq) {
93
+ if (!signals.some((s) => s.code === PROGRESS_SIGNAL.REPEATED_FAILURE_WITH_SAME_DIAGNOSIS && s.requirement === req)) {
94
+ signals.push({
95
+ code: PROGRESS_SIGNAL.REPEATED_FAILURE_WITH_SAME_DIAGNOSIS,
96
+ severity: "BLOCKING_FOR_RETRY",
97
+ message: `Requirement "${req}" failed in ${cyclesSet.size} cycles and the latest diagnoses contain no new information.`,
98
+ verificationCycles: sortedCycles,
99
+ requirement: req,
100
+ });
101
+ }
102
+ status = PROGRESS_STATUS.STALLED;
103
+ } else {
104
+ if (!signals.some((s) => s.code === PROGRESS_SIGNAL.REPEATED_FAILED_REQUIREMENT && s.requirement === req)) {
105
+ signals.push({
106
+ code: PROGRESS_SIGNAL.REPEATED_FAILED_REQUIREMENT,
107
+ severity: "ADVISORY",
108
+ message: `Requirement "${req}" failed across ${cyclesSet.size} distinct verification cycles.`,
109
+ verificationCycles: sortedCycles,
110
+ requirement: req,
111
+ });
112
+ }
113
+ if (status !== PROGRESS_STATUS.STALLED) {
114
+ status = PROGRESS_STATUS.WATCH;
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ // 3. High cycle count
121
+ const currentCycle = state.verificationCycle ?? 1;
122
+ if (currentCycle >= 4) {
123
+ signals.push({
124
+ code: PROGRESS_SIGNAL.HIGH_CORRECTION_CYCLE_COUNT,
125
+ severity: "ADVISORY",
126
+ message: `Task has reached verification cycle ${currentCycle}.`,
127
+ verificationCycles: Array.from({ length: currentCycle }, (_, i) => i + 1),
128
+ });
129
+ if (status !== PROGRESS_STATUS.STALLED) {
130
+ status = PROGRESS_STATUS.WATCH;
131
+ }
132
+ }
133
+
134
+ // Sort signals deterministically
135
+ signals.sort((left, right) =>
136
+ left.code.localeCompare(right.code) || (left.requirement || "").localeCompare(right.requirement || "")
137
+ );
138
+
139
+ return {
140
+ status,
141
+ signals,
142
+ };
143
+ }
@@ -93,6 +93,14 @@ export const FAILURE_CODES = Object.freeze([
93
93
  "E_CONTINUITY_CONTRACT_MISMATCH",
94
94
  "E_CONTINUITY_PHASE_MISMATCH",
95
95
  "E_CONTINUITY_RECONCILIATION_REQUIRED",
96
+ "E_DIAGNOSIS_REQUIRED",
97
+ "E_DIAGNOSIS_INVALID",
98
+ "E_DIAGNOSIS_EVIDENCE_INVALID",
99
+ "E_DIAGNOSIS_CYCLE_MISMATCH",
100
+ "E_DIAGNOSIS_NO_NEW_INFORMATION",
101
+ "E_PROGRESS_STALLED",
102
+ "E_DECISION_CRITERION_INVALID",
103
+ "E_DECISION_NOT_UNRESOLVED",
96
104
  ]);
97
105
 
98
106
  export const WORK_PHASES = Object.freeze([
@@ -0,0 +1,173 @@
1
+ import { readContract } from "./contract.js";
2
+ import { appendProtocolEvent, validateEventLedger } from "./events.js";
3
+ import { runCommandExecution } from "./execution.js";
4
+ import { currentRepositoryFingerprint } from "./repository.js";
5
+ import { taskArtifactPath } from "./task-paths.js";
6
+ import { classifyLoadedWorkState, readWorkState, writeWorkState } from "./work-state.js";
7
+
8
+ export const RECONCILE_EVENT = "CHECKPOINT_RECONCILED";
9
+
10
+ const RECONCILABLE_DRIFT = new Set(["REPOSITORY_CHANGED"]);
11
+
12
+ function reconcileError(code, message, artifacts = []) {
13
+ const error = new Error(message);
14
+ error.code = code;
15
+ error.artifacts = artifacts;
16
+ return error;
17
+ }
18
+
19
+ /**
20
+ * Canonical recovery for an EXECUTING task whose objective is already
21
+ * satisfied in the current repository but whose work-state checkpoint is
22
+ * stale because the repository fingerprint moved.
23
+ *
24
+ * The command refreshes the checkpoint repository fingerprint only after:
25
+ * - the task is EXECUTING,
26
+ * - classification requires revalidation and the only drift is
27
+ * REPOSITORY_CHANGED,
28
+ * - the append-only event ledger is valid,
29
+ * - a contract-bound verification check (exact verification item id and
30
+ * requirement text, type VERIFICATION) executes successfully in the
31
+ * current repository as evidence that the objective is present.
32
+ *
33
+ * Closure itself proceeds through the canonical pipeline (advance to
34
+ * VERIFYING, prepare-completion, record-check, complete); claims are
35
+ * released only by canonical COMPLETE.
36
+ */
37
+ export async function runReconcileClosure({
38
+ target,
39
+ packageRoot,
40
+ taskId,
41
+ checkId,
42
+ requirement,
43
+ argv,
44
+ details,
45
+ authorityContext,
46
+ runtimeContext,
47
+ } = {}) {
48
+ if (typeof taskId !== "string" || !taskId.trim()) {
49
+ throw reconcileError("E_TASK_REQUIRED", "reconcile-closure requires --task", []);
50
+ }
51
+ if (typeof checkId !== "string" || !checkId.trim()) {
52
+ throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --id", []);
53
+ }
54
+ if (typeof requirement !== "string" || !requirement.trim()) {
55
+ throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --requirement", []);
56
+ }
57
+ if (!Array.isArray(argv) || argv.length === 0) {
58
+ throw reconcileError("E_RECONCILE_EVIDENCE_FAILED", "reconcile-closure requires -- followed by the evidence command argv", []);
59
+ }
60
+
61
+ const stateRel = taskArtifactPath(taskId, "state");
62
+ const contractRel = taskArtifactPath(taskId, "contract");
63
+ const eventsRel = taskArtifactPath(taskId, "events");
64
+
65
+ const state = await readWorkState(target, { packageRoot, taskId });
66
+ if (!state) {
67
+ throw reconcileError("E_RECONCILE_PHASE_INVALID", "Cannot reconcile without work state", [stateRel]);
68
+ }
69
+ if (state.phase !== "EXECUTING") {
70
+ throw reconcileError(
71
+ "E_RECONCILE_PHASE_INVALID",
72
+ `reconcile-closure supports EXECUTING tasks whose objective is already satisfied; found ${state.phase}`,
73
+ [stateRel],
74
+ );
75
+ }
76
+
77
+ const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
78
+ if (freshness.status !== "REVALIDATION_REQUIRED" || !freshness.reasons.includes("REPOSITORY_CHANGED")) {
79
+ throw reconcileError(
80
+ "E_RECONCILE_NOT_STALE",
81
+ `work-state checkpoint is ${freshness.status === "FRESH" ? "fresh" : "not revalidation-required"}; no reconciliation required`,
82
+ [stateRel, contractRel],
83
+ );
84
+ }
85
+ const unsupported = freshness.reasons.filter((reason) => !RECONCILABLE_DRIFT.has(reason));
86
+ if (unsupported.length > 0) {
87
+ throw reconcileError(
88
+ "E_RECONCILE_UNSUPPORTED_DRIFT",
89
+ `reconcile-closure only reconciles repository fingerprint drift; unresolved drift: ${unsupported.join(", ")}`,
90
+ [stateRel, contractRel],
91
+ );
92
+ }
93
+
94
+ const ledger = await validateEventLedger(target, packageRoot, { taskId });
95
+ if (!ledger.valid) {
96
+ const first = ledger.errors[0];
97
+ throw reconcileError(
98
+ "E_RECONCILE_LEDGER_INVALID",
99
+ `append-only event ledger must be valid before reconciliation: ${first?.message ?? "invalid ledger"}`,
100
+ [eventsRel],
101
+ );
102
+ }
103
+
104
+ const contract = await readContract(target, packageRoot, { taskId });
105
+ const verificationItem = (contract.value.verification ?? []).find(
106
+ (item) => item.type === "VERIFICATION" && item.id === checkId && item.text === requirement,
107
+ );
108
+ if (!verificationItem) {
109
+ throw reconcileError(
110
+ "E_RECONCILE_REQUIREMENT_UNKNOWN",
111
+ `no contract verification item matches id "${checkId}" with the exact requirement text`,
112
+ [contractRel],
113
+ );
114
+ }
115
+
116
+ const execution = await runCommandExecution({
117
+ target,
118
+ packageRoot,
119
+ taskId,
120
+ checkId,
121
+ requirement,
122
+ verificationCycle: state.verificationCycle ?? 1,
123
+ argv,
124
+ details,
125
+ authorityContext,
126
+ runtimeContext,
127
+ });
128
+ if (execution.execution.status !== "passed") {
129
+ throw reconcileError(
130
+ "E_RECONCILE_EVIDENCE_FAILED",
131
+ `objective-satisfaction evidence failed (exit ${execution.execution.exitCode ?? "not-started"}): ${execution.result}`,
132
+ [execution.path],
133
+ );
134
+ }
135
+
136
+ const repository = await currentRepositoryFingerprint(target);
137
+ const previous = state.repositoryFingerprint;
138
+
139
+ await appendProtocolEvent(target, {
140
+ taskId,
141
+ event: RECONCILE_EVENT,
142
+ details: {
143
+ previousBranch: previous?.branch ?? null,
144
+ previousHead: previous?.head ?? null,
145
+ currentBranch: repository.branch,
146
+ currentHead: repository.head,
147
+ checkId,
148
+ requirement,
149
+ command: argv.join(" "),
150
+ exitCode: execution.execution.exitCode ?? 0,
151
+ executionId: execution.execution.executionId,
152
+ },
153
+ }, packageRoot, { taskId });
154
+
155
+ await writeWorkState(target, {
156
+ ...state,
157
+ repositoryFingerprint: repository,
158
+ lastUpdated: new Date().toISOString(),
159
+ }, { packageRoot, taskId });
160
+
161
+ return {
162
+ taskId,
163
+ phase: state.phase,
164
+ reconciled: true,
165
+ previousRepositoryFingerprint: previous,
166
+ repositoryFingerprint: repository,
167
+ checkId,
168
+ requirement,
169
+ executionId: execution.execution.executionId,
170
+ executionPath: execution.path,
171
+ event: RECONCILE_EVENT,
172
+ };
173
+ }
@@ -24,6 +24,11 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
24
24
  "event",
25
25
  "activation",
26
26
  "policy",
27
+ "policy-rules",
28
+ "policy-discovery",
29
+ "policy-baseline",
30
+ "policy-lock",
31
+ "policy-snapshot",
27
32
  "task-bundle",
28
33
  "execution",
29
34
  "authority",
@@ -40,6 +45,7 @@ export class SchemaValidationError extends Error {
40
45
  }
41
46
 
42
47
  function typeMatches(value, type) {
48
+ if (Array.isArray(type)) return type.some((t) => typeMatches(value, t));
43
49
  if (type === "object") return value !== null && typeof value === "object" && !Array.isArray(value);
44
50
  if (type === "array") return Array.isArray(value);
45
51
  if (type === "integer") return Number.isInteger(value);
@@ -0,0 +1,85 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function normalizeDecisionText(value) {
4
+ if (typeof value !== "string" || !value.trim()) {
5
+ const error = new Error("Decision text must be a non-empty string");
6
+ error.code = "E_DECISION_CRITERION_INVALID";
7
+ throw error;
8
+ }
9
+ return value.trim().replace(/\s+/gu, " ").toLowerCase();
10
+ }
11
+
12
+ export function decisionId(decisionText) {
13
+ const normalized = normalizeDecisionText(decisionText);
14
+ const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
15
+ return `decision-${hash}`;
16
+ }
17
+
18
+ export function normalizeDecisionCriterionInput(input) {
19
+ if (!input || typeof input !== "object") {
20
+ const error = new Error("Decision criterion input must be an object");
21
+ error.code = "E_DECISION_CRITERION_INVALID";
22
+ throw error;
23
+ }
24
+ if (typeof input.decision !== "string" || !input.decision.trim()) {
25
+ const error = new Error("decision must be a non-empty string");
26
+ error.code = "E_DECISION_CRITERION_INVALID";
27
+ throw error;
28
+ }
29
+ if (typeof input.settledBy !== "string" || !input.settledBy.trim()) {
30
+ const error = new Error("settledBy must be a non-empty string");
31
+ error.code = "E_DECISION_CRITERION_INVALID";
32
+ throw error;
33
+ }
34
+ return {
35
+ decision: input.decision.trim(),
36
+ settledBy: input.settledBy.trim(),
37
+ };
38
+ }
39
+
40
+ export function assertDecisionCriterionDetails(details) {
41
+ if (!details || typeof details !== "object" || Array.isArray(details)) {
42
+ const error = new Error("Decision criterion details must be an object");
43
+ error.code = "E_DECISION_CRITERION_INVALID";
44
+ throw error;
45
+ }
46
+ if (typeof details.decision !== "string" || !details.decision.trim()) {
47
+ const error = new Error("Decision criterion decision must be a non-empty string");
48
+ error.code = "E_DECISION_CRITERION_INVALID";
49
+ throw error;
50
+ }
51
+ const expectedId = decisionId(details.decision);
52
+ if (details.decisionId !== expectedId) {
53
+ const error = new Error(`Decision criterion decisionId "${details.decisionId}" does not match computed "${expectedId}"`);
54
+ error.code = "E_DECISION_CRITERION_INVALID";
55
+ throw error;
56
+ }
57
+ if (typeof details.settledBy !== "string" || !details.settledBy.trim()) {
58
+ const error = new Error("Decision criterion settledBy must be a non-empty string");
59
+ error.code = "E_DECISION_CRITERION_INVALID";
60
+ throw error;
61
+ }
62
+ if (typeof details.contractFingerprint !== "string" || !/^[a-f0-9]{64}$/.test(details.contractFingerprint)) {
63
+ const error = new Error("Decision criterion contractFingerprint must be a 64-char hex string");
64
+ error.code = "E_DECISION_CRITERION_INVALID";
65
+ throw error;
66
+ }
67
+ return details;
68
+ }
69
+
70
+ export function decisionCriterionEvents(events, taskId) {
71
+ if (!Array.isArray(events)) return [];
72
+ return events.filter((e) => e.event === "DECISION_CRITERION_RECORDED" && (!taskId || e.taskId === taskId));
73
+ }
74
+
75
+ export function criterionForDecision(events, taskId, decisionText, contractFingerprint) {
76
+ const decId = decisionId(decisionText);
77
+ const taskEvents = decisionCriterionEvents(events, taskId);
78
+ for (let i = taskEvents.length - 1; i >= 0; i--) {
79
+ const details = taskEvents[i].details;
80
+ if (details?.decisionId === decId && (!contractFingerprint || details?.contractFingerprint === contractFingerprint)) {
81
+ return details;
82
+ }
83
+ }
84
+ return null;
85
+ }