@cassiomc1/forgeloop 1.3.0 → 1.6.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.
Files changed (163) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/AGENTS.md +1 -0
  3. package/CLAUDE.md +1 -0
  4. package/DOCS_INDEX.md +20 -8
  5. package/EXECUTION_STATE.md +60 -0
  6. package/LOOP_ENGINEERING.md +135 -5
  7. package/LOOP_SYSTEM_DESIGN.md +54 -1
  8. package/PROTOCOL_INTEGRATION.md +87 -0
  9. package/QUALITY_SCORECARD.md +2 -0
  10. package/README.md +69 -9
  11. package/TERMINOLOGY.md +15 -0
  12. package/THIRD_PARTY_NOTICES.md +30 -0
  13. package/THREAT_MODEL.md +59 -1
  14. package/docs/ARTIFACT_REFERENCE.md +183 -0
  15. package/docs/CLI_REFERENCE.md +391 -6
  16. package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
  17. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  18. package/docs/DOCUMENTATION_GUIDE.md +36 -13
  19. package/docs/EXECUTION_TRACE.md +76 -0
  20. package/docs/GETTING_STARTED.md +1 -0
  21. package/docs/MCP.md +159 -0
  22. package/docs/RECIPES.md +149 -0
  23. package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
  24. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
  25. package/docs/TROUBLESHOOTING.md +217 -3
  26. package/docs/UNIVERSAL_INTEGRATION.md +48 -0
  27. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  28. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  29. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  30. package/docs/diagrams/README.md +55 -0
  31. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  32. package/docs/diagrams/manifest.json +42 -0
  33. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  34. package/package.json +21 -8
  35. package/schemas/action.schema.json +100 -0
  36. package/schemas/approval.schema.json +51 -0
  37. package/schemas/capability-policy.schema.json +41 -0
  38. package/schemas/diagnostic-case.schema.json +85 -0
  39. package/schemas/execution-receipt.schema.json +16 -0
  40. package/schemas/hypothesis-disposition.schema.json +16 -0
  41. package/schemas/intervention.schema.json +27 -0
  42. package/schemas/policy-lock.schema.json +1 -0
  43. package/schemas/policy-snapshot.schema.json +2 -0
  44. package/schemas/task-recovery.schema.json +61 -0
  45. package/schemas/trajectory-evaluation.schema.json +64 -0
  46. package/schemas/trajectory-scenario.schema.json +42 -0
  47. package/src/cli.js +267 -347
  48. package/src/commands/action-authorize.js +41 -0
  49. package/src/commands/action-propose.js +10 -0
  50. package/src/commands/action-reconcile.js +10 -0
  51. package/src/commands/action-record.js +47 -0
  52. package/src/commands/action-show.js +10 -0
  53. package/src/commands/action-verify.js +10 -0
  54. package/src/commands/advance.js +7 -2
  55. package/src/commands/approval-request.js +64 -0
  56. package/src/commands/approval-resolve.js +10 -0
  57. package/src/commands/audit.js +5 -0
  58. package/src/commands/baseline.js +3 -3
  59. package/src/commands/eval.js +6 -0
  60. package/src/commands/history.js +18 -0
  61. package/src/commands/init.js +2 -2
  62. package/src/commands/inspect.js +55 -0
  63. package/src/commands/metrics.js +7 -0
  64. package/src/commands/next.js +8 -2
  65. package/src/commands/policy-discover.js +2 -2
  66. package/src/commands/progress.js +6 -2
  67. package/src/commands/record-diagnosis.js +37 -1
  68. package/src/commands/record-hypothesis-disposition.js +45 -0
  69. package/src/commands/record-intervention.js +35 -0
  70. package/src/commands/reflect.js +38 -0
  71. package/src/commands/report.js +9 -1
  72. package/src/commands/run-action.js +18 -0
  73. package/src/commands/status.js +17 -0
  74. package/src/commands/task-create.js +39 -1
  75. package/src/commands/task-list.js +14 -1
  76. package/src/commands/task-lock-status.js +2 -2
  77. package/src/commands/task-recover.js +202 -0
  78. package/src/commands/task-repair-legacy-recovery.js +417 -0
  79. package/src/commands/task-resume.js +172 -0
  80. package/src/commands/task-scope.js +23 -4
  81. package/src/commands/task-show.js +18 -4
  82. package/src/commands/trace.js +34 -0
  83. package/src/commands/validate-protocol.js +40 -15
  84. package/src/core/action-authorization.js +106 -0
  85. package/src/core/action-constants.js +86 -0
  86. package/src/core/action-execution.js +105 -0
  87. package/src/core/action-ledger-projection.js +302 -0
  88. package/src/core/action-model.js +581 -0
  89. package/src/core/action-readiness.js +141 -0
  90. package/src/core/action-reconciliation-policy.js +49 -0
  91. package/src/core/action-reconciliation.js +66 -0
  92. package/src/core/action-verification.js +111 -0
  93. package/src/core/actions.js +462 -0
  94. package/src/core/approvals.js +405 -0
  95. package/src/core/artifact-registry.js +60 -0
  96. package/src/core/audit.js +45 -4
  97. package/src/core/bundles.js +30 -0
  98. package/src/core/capability-policy.js +226 -0
  99. package/src/core/cli-command-definitions.js +260 -5
  100. package/src/core/command-executors.js +543 -0
  101. package/src/core/command-input.js +107 -0
  102. package/src/core/command-runtime.js +117 -0
  103. package/src/core/completion-artifacts.js +39 -15
  104. package/src/core/completion-ownership.js +88 -0
  105. package/src/core/completion-recovery-rebind.js +194 -0
  106. package/src/core/completion.js +70 -0
  107. package/src/core/continuity-reconciliation.js +24 -5
  108. package/src/core/diagnostic-model.js +396 -0
  109. package/src/core/diagnostic-projection.js +51 -0
  110. package/src/core/diagnostic-record.js +360 -0
  111. package/src/core/error-codes.js +461 -1
  112. package/src/core/events.js +171 -2
  113. package/src/core/execution-prerequisites.js +4 -1
  114. package/src/core/execution.js +26 -188
  115. package/src/core/failure-signature.js +70 -0
  116. package/src/core/failure-surface.js +57 -0
  117. package/src/core/filesystem.js +55 -6
  118. package/src/core/history.js +110 -0
  119. package/src/core/hypothesis-projection.js +85 -0
  120. package/src/core/information-gain-projection.js +283 -0
  121. package/src/core/information-gain.js +138 -0
  122. package/src/core/inspect.js +132 -7
  123. package/src/core/integration-invocation-policy.js +217 -0
  124. package/src/core/integration-limits.js +20 -0
  125. package/src/core/integration-resources.js +178 -0
  126. package/src/core/next-action-model.js +94 -0
  127. package/src/core/next-action.js +490 -3
  128. package/src/core/phase.js +42 -22
  129. package/src/core/policy-engine.js +113 -6
  130. package/src/core/preflight-consistency.js +31 -5
  131. package/src/core/preflight.js +19 -2
  132. package/src/core/prepared-execution.js +227 -0
  133. package/src/core/progress.js +41 -4
  134. package/src/core/project-root.js +21 -0
  135. package/src/core/protocol-info.js +61 -0
  136. package/src/core/protocol.js +14 -0
  137. package/src/core/receipt.js +1 -0
  138. package/src/core/reconcile-closure.js +35 -10
  139. package/src/core/recovery-history.js +116 -0
  140. package/src/core/reflection.js +305 -0
  141. package/src/core/resumability.js +57 -3
  142. package/src/core/schema-validation.js +9 -0
  143. package/src/core/strategy-analysis.js +97 -0
  144. package/src/core/task-claim-state.js +272 -0
  145. package/src/core/task-command.js +5 -1
  146. package/src/core/task-conflict-inspection.js +321 -0
  147. package/src/core/task-context.js +32 -29
  148. package/src/core/task-discovery.js +14 -1
  149. package/src/core/task-lock.js +216 -22
  150. package/src/core/task-paths.js +31 -2
  151. package/src/core/task-recovery-migration.js +192 -0
  152. package/src/core/task-recovery.js +205 -0
  153. package/src/core/task-scope.js +33 -1
  154. package/src/core/task-snapshot.js +53 -0
  155. package/src/core/templates.js +9 -0
  156. package/src/core/trace.js +548 -0
  157. package/src/core/trajectory-evaluation.js +71 -0
  158. package/src/core/trajectory-metrics.js +80 -0
  159. package/src/core/transaction.js +36 -2
  160. package/src/core/work-state.js +10 -5
  161. package/src/integration.js +47 -0
  162. package/docs/assets/forgeloop-flow.svg +0 -1
  163. package/docs/forgeloop-flow.mmd +0 -51
@@ -99,6 +99,20 @@ export const FAILURE_CODES = Object.freeze([
99
99
  "E_DIAGNOSIS_CYCLE_MISMATCH",
100
100
  "E_DIAGNOSIS_NO_NEW_INFORMATION",
101
101
  "E_PROGRESS_STALLED",
102
+ "E_DIAGNOSTIC_CASE_INVALID",
103
+ "E_DIAGNOSTIC_CASE_CYCLE_MISMATCH",
104
+ "E_DIAGNOSTIC_CASE_EVIDENCE_INVALID",
105
+ "E_OBSERVATION_INVALID",
106
+ "E_CONTRIBUTOR_INVALID",
107
+ "E_CONTRIBUTOR_REFERENCE_INVALID",
108
+ "E_HYPOTHESIS_INVALID",
109
+ "E_HYPOTHESIS_SETTLEMENT_MISSING",
110
+ "E_HYPOTHESIS_DISPOSITION_INVALID",
111
+ "E_HYPOTHESIS_DISPOSITION_EVIDENCE_INVALID",
112
+ "E_INTERVENTION_INVALID",
113
+ "E_INTERVENTION_HYPOTHESIS_MISSING",
114
+ "E_INTERVENTION_REFERENCE_INVALID",
115
+ "E_STRATEGY_OSCILLATION",
102
116
  "E_DECISION_CRITERION_INVALID",
103
117
  "E_DECISION_NOT_UNRESOLVED",
104
118
  ]);
@@ -158,6 +158,7 @@ export async function createReceipt(input, packageRoot, options = {}) {
158
158
  selectedGuides: [...(input.selectedGuides ?? [])],
159
159
  changedPaths: [...(input.changedPaths ?? [])],
160
160
  checks: [...(input.checks ?? [])],
161
+ ...(input.actions !== undefined ? { actions: structuredClone(input.actions) } : {}),
161
162
  evidence: [...(input.evidence ?? [])],
162
163
  ...(input.evidenceCoverage !== undefined ? { evidenceCoverage: [...input.evidenceCoverage] } : {}),
163
164
  review: input.review ?? { status: "not-run", independent: false },
@@ -1,6 +1,7 @@
1
1
  import { readContract } from "./contract.js";
2
2
  import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
3
  import { appendProtocolEvent, validateEventLedger } from "./events.js";
4
+ import { authorizeCompletionRecoveryOrRebind } from "./completion-recovery-rebind.js";
4
5
  import { runCommandExecution } from "./execution.js";
5
6
  import { createReceipt } from "./receipt.js";
6
7
  import { currentRepositoryFingerprint } from "./repository.js";
@@ -11,7 +12,7 @@ export const RECONCILE_EVENT = "CHECKPOINT_RECONCILED";
11
12
 
12
13
  const RECONCILABLE_DRIFT = new Set(["REPOSITORY_CHANGED"]);
13
14
 
14
- const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING"]);
15
+ const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING", "REVIEWING"]);
15
16
 
16
17
  function reconcileError(code, message, artifacts = []) {
17
18
  const error = new Error(message);
@@ -21,12 +22,13 @@ function reconcileError(code, message, artifacts = []) {
21
22
  }
22
23
 
23
24
  /**
24
- * Canonical recovery for an EXECUTING or VERIFYING task whose objective is
25
- * already satisfied in the current repository but whose work-state
26
- * checkpoint is stale because the repository fingerprint moved.
25
+ * Canonical recovery for an EXECUTING, VERIFYING, or REVIEWING task whose
26
+ * objective is already satisfied in the current repository but whose
27
+ * work-state checkpoint is stale because the repository fingerprint moved.
27
28
  *
28
29
  * The command refreshes the checkpoint repository fingerprint only after:
29
- * - the task is EXECUTING or VERIFYING,
30
+ * - the task is EXECUTING, VERIFYING, or (with authorized completion
31
+ * recovery) REVIEWING,
30
32
  * - classification requires revalidation and the only drift is
31
33
  * REPOSITORY_CHANGED,
32
34
  * - the append-only event ledger is valid,
@@ -67,17 +69,37 @@ export async function runReconcileClosure({
67
69
  const eventsRel = taskArtifactPath(taskId, "events");
68
70
  const receiptRel = taskArtifactPath(taskId, "receipt");
69
71
 
70
- const state = await readWorkState(target, { packageRoot, taskId });
72
+ let state = await readWorkState(target, { packageRoot, taskId });
71
73
  if (!state) {
72
74
  throw reconcileError("E_RECONCILE_PHASE_INVALID", "Cannot reconcile without work state", [stateRel]);
73
75
  }
74
76
  if (!RECONCILABLE_PHASES.has(state.phase)) {
75
77
  throw reconcileError(
76
78
  "E_RECONCILE_PHASE_INVALID",
77
- `reconcile-closure supports EXECUTING or VERIFYING tasks whose objective is already satisfied; found ${state.phase}`,
79
+ `reconcile-closure supports EXECUTING, VERIFYING, or REVIEWING tasks whose objective is already satisfied; found ${state.phase}`,
78
80
  [stateRel],
79
81
  );
80
82
  }
83
+ if (state.phase === "REVIEWING") {
84
+ const recovery = await authorizeCompletionRecoveryOrRebind({
85
+ target,
86
+ packageRoot,
87
+ taskId,
88
+ authorityContext,
89
+ runtimeContext,
90
+ });
91
+ if (!recovery.recoveryAuth.authorized) {
92
+ const first = recovery.recoveryAuth.errors?.[0] ?? {};
93
+ throw reconcileError(
94
+ first.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
95
+ `REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
96
+ [stateRel, receiptRel],
97
+ );
98
+ }
99
+ if (recovery.rebound) {
100
+ state = recovery.state;
101
+ }
102
+ }
81
103
 
82
104
  const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
83
105
  if (freshness.status !== "REVALIDATION_REQUIRED" || !freshness.reasons.includes("REPOSITORY_CHANGED")) {
@@ -107,9 +129,12 @@ export async function runReconcileClosure({
107
129
  }
108
130
 
109
131
  const contract = await readContract(target, packageRoot, { taskId });
110
- const verificationItem = (contract.value.verification ?? []).find(
111
- (item) => item.type === "VERIFICATION" && item.id === checkId && item.text === requirement,
112
- );
132
+ const verificationItem = (contract.value.verification ?? []).find((item) => {
133
+ if (typeof item === "string") {
134
+ return item === requirement;
135
+ }
136
+ return item.type === "VERIFICATION" && item.id === checkId && item.text === requirement;
137
+ });
113
138
  if (!verificationItem) {
114
139
  throw reconcileError(
115
140
  "E_RECONCILE_REQUIREMENT_UNKNOWN",
@@ -0,0 +1,116 @@
1
+ import {
2
+ LEGACY_RECOVERY_MIGRATION_EVENT,
3
+ isLegacyRecoveryEventShape,
4
+ } from "./task-recovery-migration.js";
5
+
6
+ const RECOVERY_EVENT_TYPES = new Set([
7
+ "TASK_RECOVERY_RECORDED",
8
+ "OPERATOR_RECOVERY_RECORDED",
9
+ LEGACY_RECOVERY_MIGRATION_EVENT,
10
+ ]);
11
+
12
+ function recoveryError(message) {
13
+ return {
14
+ code: "E_TASK_RECOVERY_INCONSISTENT",
15
+ message,
16
+ };
17
+ }
18
+
19
+ function recoveryIdOf(event) {
20
+ return typeof event?.details?.recoveryId === "string" && event.details.recoveryId !== ""
21
+ ? event.details.recoveryId
22
+ : null;
23
+ }
24
+
25
+ export function classifyRecoveryHistory(events = []) {
26
+ const recoveries = [];
27
+ const completedRecoveries = [];
28
+ const errors = [];
29
+ const seenRecoveryIds = new Set();
30
+ let activeCycle = null;
31
+
32
+ for (const event of events) {
33
+ if (RECOVERY_EVENT_TYPES.has(event?.event)) {
34
+ // The known legacy defect signature is never an owning recovery cycle by
35
+ // itself. It remains non-owning historical evidence; ownership comes
36
+ // exclusively from its valid LEGACY_RECOVERY_MIGRATION_RECORDED event,
37
+ // which is validated (binding, uniqueness) by the ledger validator.
38
+ if (!event.details?.recoveryId && isLegacyRecoveryEventShape(event)) continue;
39
+ // A migration event only counts as a canonical recovery cycle when it
40
+ // binds an actual legacy event present earlier in this ledger.
41
+ if (event.event === LEGACY_RECOVERY_MIGRATION_EVENT
42
+ && !events.some((candidate) => candidate.seq === event.details?.legacyEventSeq
43
+ && isLegacyRecoveryEventShape(candidate))) {
44
+ errors.push(recoveryError(
45
+ `Legacy migration event at seq ${event?.seq ?? "unknown"} does not bind a legacy recovery event in this ledger`,
46
+ ));
47
+ continue;
48
+ }
49
+ const recoveryId = recoveryIdOf(event);
50
+ if (!recoveryId) {
51
+ errors.push(recoveryError(`Recovery event at seq ${event?.seq ?? "unknown"} has no recoveryId`));
52
+ continue;
53
+ }
54
+ if (seenRecoveryIds.has(recoveryId)) {
55
+ errors.push(recoveryError(`Recovery event at seq ${event.seq} reuses recovery id ${recoveryId}`));
56
+ continue;
57
+ }
58
+ if (activeCycle) {
59
+ errors.push(recoveryError(
60
+ `Recovery ${recoveryId} was recorded while recovery ${activeCycle.recoveryId} is unresolved`,
61
+ ));
62
+ continue;
63
+ }
64
+
65
+ activeCycle = {
66
+ recoveryId,
67
+ recoveryEventSeq: event.seq,
68
+ resumedEventSeq: null,
69
+ active: true,
70
+ event,
71
+ resumedEvent: null,
72
+ };
73
+ seenRecoveryIds.add(recoveryId);
74
+ recoveries.push(activeCycle);
75
+ continue;
76
+ }
77
+
78
+ if (event?.event !== "TASK_RECOVERY_RESUMED") continue;
79
+
80
+ const recoveryId = recoveryIdOf(event);
81
+ if (!recoveryId || !activeCycle || activeCycle.recoveryId !== recoveryId) {
82
+ errors.push(recoveryError(
83
+ `TASK_RECOVERY_RESUMED at seq ${event?.seq ?? "unknown"} does not reference an active recovery`,
84
+ ));
85
+ continue;
86
+ }
87
+ if (!Number.isInteger(event.seq) || event.seq <= activeCycle.recoveryEventSeq) {
88
+ errors.push(recoveryError(
89
+ `Resume sequence for recovery ${recoveryId} must be greater than recovery sequence ${activeCycle.recoveryEventSeq}`,
90
+ ));
91
+ continue;
92
+ }
93
+
94
+ activeCycle.resumedEventSeq = event.seq;
95
+ activeCycle.resumedEvent = event;
96
+ activeCycle.active = false;
97
+ completedRecoveries.push(activeCycle);
98
+ activeCycle = null;
99
+ }
100
+
101
+ const valid = errors.length === 0;
102
+ return {
103
+ recoveries,
104
+ activeRecoveryId: valid ? activeCycle?.recoveryId ?? null : null,
105
+ activeRecovery: valid && activeCycle
106
+ ? { recoveryId: activeCycle.recoveryId, event: activeCycle.event }
107
+ : null,
108
+ completedRecoveries,
109
+ valid,
110
+ errors,
111
+ };
112
+ }
113
+
114
+ export function resolveRecoveryHistory(events = []) {
115
+ return classifyRecoveryHistory(events);
116
+ }
@@ -0,0 +1,305 @@
1
+ import { buildTaskTrace } from "./trace.js";
2
+ import { readEvents } from "./events.js";
3
+ import { findTaskById } from "./task-discovery.js";
4
+ import { projectHypothesisStates } from "./hypothesis-projection.js";
5
+ import {
6
+ buildInformationGainProjection,
7
+ evaluateStructuredDiagnosticStall,
8
+ } from "./information-gain-projection.js";
9
+ import { computeFailureSignature } from "./failure-signature.js";
10
+ import {
11
+ computeStrategyFingerprints as computeStrategyFingerprintsImpl,
12
+ detectOscillation as detectOscillationImpl,
13
+ evaluateInterventionEffectiveness as evaluateInterventionEffectivenessImpl,
14
+ } from "./strategy-analysis.js";
15
+
16
+ export const REFLECTION_STATUS = Object.freeze({
17
+ ADVANCING: "ADVANCING",
18
+ WATCH: "WATCH",
19
+ STALLED: "STALLED",
20
+ });
21
+
22
+
23
+ export function summarizeHypotheses(trace) {
24
+ const summary = { created: 0, supported: 0, weakened: 0, falsified: 0, superseded: 0, unresolved: 0, open: 0 };
25
+
26
+ const pseudoEvents = [
27
+ ...trace.diagnostics.cases.map((diagnosticCase) => ({
28
+ seq: diagnosticCase.sequence,
29
+ event: "DIAGNOSTIC_CASE_RECORDED",
30
+ details: diagnosticCase,
31
+ })),
32
+ ...trace.diagnostics.legacyDiagnoses.map((legacy) => ({
33
+ seq: legacy.sequence,
34
+ event: "DIAGNOSIS_RECORDED",
35
+ details: legacy,
36
+ })),
37
+ ...trace.diagnostics.dispositions.map((disposition) => ({
38
+ seq: disposition.sequence,
39
+ event: "HYPOTHESIS_DISPOSITION_RECORDED",
40
+ details: disposition,
41
+ })),
42
+ ];
43
+ const projection = projectHypothesisStates(pseudoEvents);
44
+
45
+ for (const hypothesis of projection.hypotheses) {
46
+ if (hypothesis.id === "h-legacy" && hypothesis.sourceEventSeq !== null) {
47
+ summary.created += 1;
48
+ } else if (hypothesis.id !== "h-legacy") {
49
+ summary.created += 1;
50
+ }
51
+ const status = hypothesis.currentStatus.toLowerCase();
52
+ if (summary[status] !== undefined) summary[status] += 1;
53
+ }
54
+ summary.open = projection.openHypotheses.length;
55
+ return summary;
56
+ }
57
+
58
+ export function detectOscillation(strategies) {
59
+ return detectOscillationImpl(strategies);
60
+ }
61
+
62
+ export function evaluateInterventionEffectiveness(trace, surfacesByCycle) {
63
+ return evaluateInterventionEffectivenessImpl(trace, surfacesByCycle);
64
+ }
65
+
66
+ function failedRequirementSurfacesFromEvents(events = [], state = null) {
67
+ const byCycle = new Map();
68
+ const record = (cycle, requirement) => {
69
+ if (!requirement) return;
70
+ if (!byCycle.has(cycle)) byCycle.set(cycle, new Set());
71
+ byCycle.get(cycle).add(requirement);
72
+ };
73
+ for (const event of events) {
74
+ if (event.event !== "VERIFICATION_RECORDED") continue;
75
+ const d = event.details ?? {};
76
+ if (d.status === "failed" || d.status === "blocked") {
77
+ record(d.verificationCycle ?? 1, d.requirement ?? d.id ?? d.checkId);
78
+ }
79
+ }
80
+ for (const check of state?.checks ?? []) {
81
+ if (check.status !== "failed" && check.status !== "blocked") continue;
82
+ record(check.details?.verificationCycle ?? state?.verificationCycle ?? 1, check.requirement ?? check.id ?? check.checkId);
83
+ }
84
+ return byCycle;
85
+ }
86
+
87
+ function signatureSetsByCycle(trace) {
88
+ const byCycle = new Map();
89
+ for (const entry of trace.failureSignatures) {
90
+ for (const cycle of entry.cycles ?? []) {
91
+ if (!byCycle.has(cycle)) byCycle.set(cycle, new Set());
92
+ byCycle.get(cycle).add(entry.signature);
93
+ }
94
+ }
95
+ return byCycle;
96
+ }
97
+
98
+ export function deriveDiagnosticContext(events = [], state = null) {
99
+ const cycle = state?.verificationCycle ?? null;
100
+ const taskEvents = events.filter((event) => !state?.taskId || event.taskId === state.taskId);
101
+
102
+ // Canonical failure-signature hashes for the active verification cycle.
103
+ const activeFailureSignatures = [...new Set(
104
+ taskEvents
105
+ .filter((event) => event.event === "VERIFICATION_RECORDED"
106
+ && ["failed", "blocked"].includes(event.details?.status)
107
+ && (cycle === null || event.details?.verificationCycle === cycle))
108
+ .map((event) => {
109
+ const d = event.details;
110
+ return computeFailureSignature({
111
+ requirement: d.requirement ?? d.id ?? d.checkId,
112
+ status: d.status,
113
+ exitCode: Number.isInteger(d.exitCode) ? d.exitCode : null,
114
+ failureToken: typeof d.failureToken === "string" && d.failureToken
115
+ ? d.failureToken
116
+ : (typeof d.details?.failureToken === "string" ? d.details.failureToken : null),
117
+ });
118
+ }),
119
+ )].sort();
120
+
121
+ const activeFailedRequirements = [...new Set(
122
+ [
123
+ ...taskEvents
124
+ .filter((event) => event.event === "VERIFICATION_RECORDED"
125
+ && ["failed", "blocked"].includes(event.details?.status)
126
+ && (cycle === null || event.details?.verificationCycle === cycle))
127
+ .map((event) => event.details?.requirement ?? event.details?.id ?? event.details?.checkId),
128
+ ...((state?.checks ?? []))
129
+ .filter((check) => ["failed", "blocked"].includes(check.status)
130
+ && (cycle === null || check.details?.verificationCycle === cycle))
131
+ .map((check) => check.requirement ?? check.id ?? check.checkId),
132
+ ].filter(Boolean),
133
+ )].sort();
134
+
135
+ const projection = projectHypothesisStates(taskEvents);
136
+ const openHypotheses = [...projection.openHypotheses].sort();
137
+
138
+ const interventions = taskEvents.filter((event) => event.event === "INTERVENTION_RECORDED");
139
+ const latestIntervention = interventions.at(-1)?.details?.intervention?.id ?? null;
140
+
141
+ // doNotRepeat requires semantic repetition AND at least two completed
142
+ // post-intervention verification cycles AND unchanged failure surface.
143
+ const surfacesByCycle = failedRequirementSurfacesFromEvents(taskEvents, state);
144
+ const completedCycles = [...new Set(
145
+ taskEvents.filter((event) => event.event === "VERIFICATION_STARTED")
146
+ .map((event) => event.details?.verificationCycle)
147
+ .filter(Number.isInteger),
148
+ )].sort((a, b) => a - b);
149
+
150
+ const fingerprintGroups = new Map();
151
+ for (const event of interventions) {
152
+ const fingerprint = event.details?.interventionSemanticFingerprint;
153
+ if (!fingerprint) continue;
154
+ if (!fingerprintGroups.has(fingerprint)) fingerprintGroups.set(fingerprint, []);
155
+ fingerprintGroups.get(fingerprint).push(event);
156
+ }
157
+
158
+ const doNotRepeat = [];
159
+ for (const [fingerprint, group] of fingerprintGroups.entries()) {
160
+ if (group.length < 2) continue;
161
+ const lastInterventionCycle = Math.max(...group.map((event) => event.details?.verificationCycle ?? 1));
162
+ const postCycles = completedCycles.filter((completed) => completed > lastInterventionCycle);
163
+ if (postCycles.length < 2) continue;
164
+ const firstSurface = [...(surfacesByCycle.get(lastInterventionCycle) ?? [])].sort();
165
+ const latestSurface = [...(surfacesByCycle.get(postCycles.at(-1)) ?? [])].sort();
166
+ if (JSON.stringify(firstSurface) === JSON.stringify(latestSurface)) {
167
+ doNotRepeat.push(fingerprint);
168
+ }
169
+ }
170
+ doNotRepeat.sort();
171
+
172
+ return {
173
+ activeFailureSignatures,
174
+ activeFailedRequirements,
175
+ openHypotheses,
176
+ latestIntervention,
177
+ nextExperiment: null,
178
+ doNotRepeat,
179
+ };
180
+ }
181
+
182
+ export async function buildTaskReflection({ target, packageRoot, taskId = null } = {}) {
183
+ const trace = await buildTaskTrace({ target, packageRoot, taskId });
184
+ // Modern tasks own a scoped ledger; legacy singleton callers still pass a
185
+ // taskId that must be used only as a filter over the canonical ledger.
186
+ const task = taskId ? await findTaskById(target, taskId, packageRoot) : null;
187
+ const rawEvents = await readEvents(target, packageRoot, task ? { taskId } : {});
188
+
189
+ // Authoritative surfaces come from the canonical trace projection.
190
+ const surfaceEntries = {};
191
+ for (const entry of trace.failureSurfaces) {
192
+ surfaceEntries[entry.verificationCycle] = { surface: entry.surface, signatures: [] };
193
+ }
194
+ const signatureSets = signatureSetsByCycle(trace);
195
+ for (const [cycle, signatures] of signatureSets.entries()) {
196
+ if (!surfaceEntries[cycle]) surfaceEntries[cycle] = { surface: [], signatures: [] };
197
+ surfaceEntries[cycle].signatures = [...signatures].sort();
198
+ }
199
+
200
+ const cycles = [...new Set([
201
+ ...trace.failureSurfaces.map((entry) => entry.verificationCycle),
202
+ ...trace.diagnostics.cases.map((diagnosticCase) => diagnosticCase.verificationCycle),
203
+ ])].sort((a, b) => a - b);
204
+
205
+ const strategies = computeStrategyFingerprintsImpl(trace);
206
+ const oscillation = detectOscillation(strategies);
207
+
208
+ const signals = [];
209
+ let status = REFLECTION_STATUS.ADVANCING;
210
+
211
+ const evaluatedInterventions = evaluateInterventionEffectiveness(trace, surfaceEntries);
212
+ const repeatedNonInformative = evaluatedInterventions
213
+ .filter((intervention) => intervention.effectiveness === "NON_INFORMATIVE");
214
+ if (oscillation.detected) {
215
+ signals.push("OSCILLATING_STRATEGY");
216
+ status = REFLECTION_STATUS.WATCH;
217
+ }
218
+ if (repeatedNonInformative.length >= 2) {
219
+ signals.push("REPEATED_INTERVENTION");
220
+ status = REFLECTION_STATUS.WATCH;
221
+ }
222
+
223
+ const lastTwoSameStrategy = strategies.length >= 2
224
+ && strategies.at(-1).strategyFingerprint === strategies.at(-2).strategyFingerprint
225
+ && JSON.stringify(surfaceEntries[strategies.at(-1)?.verificationCycle]?.signatures ?? [])
226
+ === JSON.stringify(surfaceEntries[strategies.at(-2)?.verificationCycle]?.signatures ?? []);
227
+ if (lastTwoSameStrategy && !signals.includes("OSCILLATING_STRATEGY")) {
228
+ status = status === REFLECTION_STATUS.ADVANCING ? REFLECTION_STATUS.WATCH : status;
229
+ }
230
+
231
+ // Information Gain v2 truth comes fully from the authoritative cycle
232
+ // analysis projection. Consumers must not redefine gain semantics.
233
+ const gainProjection = buildInformationGainProjection(rawEvents, taskId ?? null);
234
+ const cyclesWithoutEffectiveGain = gainProjection
235
+ .filter((entry) => !entry.effectiveGain)
236
+ .map((entry) => entry.verificationCycle);
237
+
238
+ // One canonical structured-stall truth shared with phase and progress.
239
+ const stallAnalysis = evaluateStructuredDiagnosticStall(gainProjection);
240
+ const latestNoGain = Boolean(stallAnalysis.stalled);
241
+ let consecutiveNoGainCycles = 0;
242
+ for (let i = gainProjection.length - 1; i >= 0 && gainProjection[i].effectiveGain === false; i--) {
243
+ consecutiveNoGainCycles += 1;
244
+ }
245
+ const previousEntry = gainProjection.length >= 2 ? gainProjection.at(-2) : null;
246
+ const currentEntry = gainProjection.at(-1) ?? null;
247
+ if (latestNoGain) {
248
+ status = REFLECTION_STATUS.STALLED;
249
+ if (!signals.includes("NO_EFFECTIVE_INFORMATION_GAIN")) signals.push("NO_EFFECTIVE_INFORMATION_GAIN");
250
+ }
251
+ if ((trace.actions?.ambiguous ?? 0) > 0) {
252
+ signals.push("EXTERNAL_ACTION_RECONCILIATION_REQUIRED");
253
+ status = status === REFLECTION_STATUS.ADVANCING ? REFLECTION_STATUS.WATCH : status;
254
+ }
255
+
256
+ const recommendedProtocolAction = (trace.actions?.ambiguous ?? 0) > 0
257
+ ? "RECONCILE_EXTERNAL_ACTION"
258
+ : oscillation.detected
259
+ ? "INTRODUCE_NEW_OBSERVATION"
260
+ : (status === REFLECTION_STATUS.STALLED ? "REQUIRE_NEW_DIAGNOSTIC_INFORMATION" : "CONTINUE");
261
+
262
+ return {
263
+ schemaVersion: 1,
264
+ command: "reflect",
265
+ taskId: trace.task.id,
266
+ taskPhase: trace.task.phase,
267
+ integrityValid: trace.integrity.valid,
268
+ snapshotConsistent: trace.snapshot.consistent,
269
+ status,
270
+ verificationCycles: cycles.length,
271
+ failureSurfaces: trace.failureSurfaces,
272
+ hypotheses: summarizeHypotheses(trace),
273
+ interventions: (() => {
274
+ const evaluated = evaluatedInterventions;
275
+ return {
276
+ count: evaluated.length,
277
+ informative: evaluated.filter((intervention) => ["INFORMATIVE", "IMPROVED"].includes(intervention.effectiveness)).length,
278
+ nonInformative: evaluated.filter((intervention) => intervention.effectiveness === "NON_INFORMATIVE").length,
279
+ details: evaluated,
280
+ };
281
+ })(),
282
+ informationGain: {
283
+ cyclesWithCases: trace.diagnostics.cases.map((diagnosticCase) => diagnosticCase.verificationCycle),
284
+ cyclesWithoutEffectiveGain,
285
+ cycles: gainProjection.map(({ dimensions, ...rest }) => ({ ...rest, dimensions })),
286
+ },
287
+ strategies,
288
+ oscillation,
289
+ signals,
290
+ stallAnalysis: {
291
+ latestNoGain,
292
+ consecutiveNoGainCycles,
293
+ sameStrategyAsPrevious: Boolean(previousEntry && currentEntry
294
+ && previousEntry.evidence.strategyFingerprint === currentEntry.evidence.strategyFingerprint),
295
+ sameFailureSurfaceAsPrevious: Boolean(previousEntry && currentEntry
296
+ && JSON.stringify(previousEntry.evidence.failureSurface) === JSON.stringify(currentEntry.evidence.failureSurface)),
297
+ sameFailureSignaturesAsPrevious: Boolean(previousEntry && currentEntry
298
+ && JSON.stringify(previousEntry.evidence.failureSignatures) === JSON.stringify(currentEntry.evidence.failureSignatures)),
299
+ },
300
+ recommendedProtocolAction,
301
+ actions: trace.actions,
302
+ };
303
+ }
304
+
305
+ export { computeStrategyFingerprintsImpl as computeStrategyFingerprints };
@@ -1,22 +1,76 @@
1
1
  import { currentRepositoryFingerprint } from "./repository.js";
2
+ import { validateEventLedger } from "./events.js";
2
3
  import { createWorkState, initializeWorkState, readWorkState, mutateWorkState } from "./work-state.js";
3
4
 
4
5
  const DEFAULT_PENDING_STEPS = ["planning", "implementation", "verification"];
5
6
 
7
+ /**
8
+ * Resume phase derived from the highest lifecycle milestone already recorded in
9
+ * a validated ledger. Recreating a checkpoint at ROUTED for a task whose ledger
10
+ * already passed EXECUTION_STARTED would make every subsequent advance append a
11
+ * duplicate non-repeatable milestone and invalidate the ledger, so restoration
12
+ * must resume at the phase the recorded chronology supports.
13
+ */
14
+ const RESUME_PHASE_BY_MILESTONE = Object.freeze({
15
+ PLAN_RECORDED: "PLANNED",
16
+ EXECUTION_STARTED: "EXECUTING",
17
+ VERIFICATION_STARTED: "VERIFYING",
18
+ VERIFICATION_RECORDED: "VERIFYING",
19
+ });
20
+
21
+ async function deriveResumePhaseFromLedger(target, packageRoot, taskId) {
22
+ let ledger;
23
+ try {
24
+ ledger = await validateEventLedger(target, packageRoot, { taskId });
25
+ } catch {
26
+ return null;
27
+ }
28
+ if (!ledger?.valid) return null;
29
+ const scoped = (ledger.events ?? []).filter((event) => event.taskId === taskId);
30
+ if (scoped.some((event) => event.event === "COMPLETION_VALIDATED")) return null;
31
+ const positions = RESUME_PHASE_BY_MILESTONE;
32
+ let derived = null;
33
+ for (const event of scoped) {
34
+ const phase = positions[event.event];
35
+ if (!phase) continue;
36
+ if (!derived) {
37
+ derived = phase;
38
+ continue;
39
+ }
40
+ if (phase === "VERIFYING") derived = "VERIFYING";
41
+ }
42
+ return derived;
43
+ }
44
+
45
+ function resumeSteps(phase) {
46
+ if (phase === "EXECUTING" || phase === "VERIFYING") {
47
+ return {
48
+ completedSteps: ["contract", "route", "planning", "implementation"],
49
+ pendingSteps: ["verification"],
50
+ };
51
+ }
52
+ return {
53
+ completedSteps: ["contract", "route"],
54
+ pendingSteps: [...DEFAULT_PENDING_STEPS],
55
+ };
56
+ }
57
+
6
58
  export async function ensureResumableState({ target, packageRoot, contract, route, taskId, statePath }) {
7
59
  if (!contract || !route) return null;
8
60
  const existing = await readWorkState(target, { packageRoot, taskId, statePath });
9
61
  if (existing) return existing;
10
62
 
63
+ const resumedPhase = await deriveResumePhaseFromLedger(target, packageRoot, taskId) ?? "ROUTED";
64
+ const steps = resumeSteps(resumedPhase);
11
65
  const state = createWorkState({
12
66
  taskId: contract.value.taskId,
13
67
  contractFingerprint: contract.fingerprint,
14
68
  routeFingerprint: route.fingerprint,
15
69
  repositoryFingerprint: await currentRepositoryFingerprint(target),
16
- phase: "ROUTED",
70
+ phase: resumedPhase,
17
71
  selectedGuides: route.value.guides,
18
- completedSteps: ["contract", "route"],
19
- pendingSteps: DEFAULT_PENDING_STEPS,
72
+ completedSteps: steps.completedSteps,
73
+ pendingSteps: steps.pendingSteps,
20
74
  checks: [],
21
75
  failures: [],
22
76
  blockers: [],
@@ -33,6 +33,15 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
33
33
  "execution",
34
34
  "authority",
35
35
  "task-descriptor",
36
+ "task-recovery",
37
+ "diagnostic-case",
38
+ "intervention",
39
+ "hypothesis-disposition",
40
+ "action",
41
+ "approval",
42
+ "capability-policy",
43
+ "trajectory-evaluation",
44
+ "trajectory-scenario",
36
45
  ]);
37
46
 
38
47
  export class SchemaValidationError extends Error {