@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.
- package/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +20 -8
- package/EXECUTION_STATE.md +60 -0
- package/LOOP_ENGINEERING.md +135 -5
- package/LOOP_SYSTEM_DESIGN.md +54 -1
- package/PROTOCOL_INTEGRATION.md +87 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +69 -9
- package/TERMINOLOGY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/THREAT_MODEL.md +59 -1
- package/docs/ARTIFACT_REFERENCE.md +183 -0
- package/docs/CLI_REFERENCE.md +391 -6
- package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +36 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/GETTING_STARTED.md +1 -0
- package/docs/MCP.md +159 -0
- package/docs/RECIPES.md +149 -0
- package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
- package/docs/TROUBLESHOOTING.md +217 -3
- package/docs/UNIVERSAL_INTEGRATION.md +48 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
- package/docs/diagrams/README.md +55 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
- package/package.json +21 -8
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/hypothesis-disposition.schema.json +16 -0
- package/schemas/intervention.schema.json +27 -0
- package/schemas/policy-lock.schema.json +1 -0
- package/schemas/policy-snapshot.schema.json +2 -0
- package/schemas/task-recovery.schema.json +61 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +267 -347
- package/src/commands/action-authorize.js +41 -0
- package/src/commands/action-propose.js +10 -0
- package/src/commands/action-reconcile.js +10 -0
- package/src/commands/action-record.js +47 -0
- package/src/commands/action-show.js +10 -0
- package/src/commands/action-verify.js +10 -0
- package/src/commands/advance.js +7 -2
- package/src/commands/approval-request.js +64 -0
- package/src/commands/approval-resolve.js +10 -0
- package/src/commands/audit.js +5 -0
- package/src/commands/baseline.js +3 -3
- package/src/commands/eval.js +6 -0
- package/src/commands/history.js +18 -0
- package/src/commands/init.js +2 -2
- package/src/commands/inspect.js +55 -0
- package/src/commands/metrics.js +7 -0
- package/src/commands/next.js +8 -2
- package/src/commands/policy-discover.js +2 -2
- package/src/commands/progress.js +6 -2
- package/src/commands/record-diagnosis.js +37 -1
- package/src/commands/record-hypothesis-disposition.js +45 -0
- package/src/commands/record-intervention.js +35 -0
- package/src/commands/reflect.js +38 -0
- package/src/commands/report.js +9 -1
- package/src/commands/run-action.js +18 -0
- package/src/commands/status.js +17 -0
- package/src/commands/task-create.js +39 -1
- package/src/commands/task-list.js +14 -1
- package/src/commands/task-lock-status.js +2 -2
- package/src/commands/task-recover.js +202 -0
- package/src/commands/task-repair-legacy-recovery.js +417 -0
- package/src/commands/task-resume.js +172 -0
- package/src/commands/task-scope.js +23 -4
- package/src/commands/task-show.js +18 -4
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +40 -15
- package/src/core/action-authorization.js +106 -0
- package/src/core/action-constants.js +86 -0
- package/src/core/action-execution.js +105 -0
- package/src/core/action-ledger-projection.js +302 -0
- package/src/core/action-model.js +581 -0
- package/src/core/action-readiness.js +141 -0
- package/src/core/action-reconciliation-policy.js +49 -0
- package/src/core/action-reconciliation.js +66 -0
- package/src/core/action-verification.js +111 -0
- package/src/core/actions.js +462 -0
- package/src/core/approvals.js +405 -0
- package/src/core/artifact-registry.js +60 -0
- package/src/core/audit.js +45 -4
- package/src/core/bundles.js +30 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +260 -5
- package/src/core/command-executors.js +543 -0
- package/src/core/command-input.js +107 -0
- package/src/core/command-runtime.js +117 -0
- package/src/core/completion-artifacts.js +39 -15
- package/src/core/completion-ownership.js +88 -0
- package/src/core/completion-recovery-rebind.js +194 -0
- package/src/core/completion.js +70 -0
- package/src/core/continuity-reconciliation.js +24 -5
- package/src/core/diagnostic-model.js +396 -0
- package/src/core/diagnostic-projection.js +51 -0
- package/src/core/diagnostic-record.js +360 -0
- package/src/core/error-codes.js +461 -1
- package/src/core/events.js +171 -2
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +26 -188
- package/src/core/failure-signature.js +70 -0
- package/src/core/failure-surface.js +57 -0
- package/src/core/filesystem.js +55 -6
- package/src/core/history.js +110 -0
- package/src/core/hypothesis-projection.js +85 -0
- package/src/core/information-gain-projection.js +283 -0
- package/src/core/information-gain.js +138 -0
- package/src/core/inspect.js +132 -7
- package/src/core/integration-invocation-policy.js +217 -0
- package/src/core/integration-limits.js +20 -0
- package/src/core/integration-resources.js +178 -0
- package/src/core/next-action-model.js +94 -0
- package/src/core/next-action.js +490 -3
- package/src/core/phase.js +42 -22
- package/src/core/policy-engine.js +113 -6
- package/src/core/preflight-consistency.js +31 -5
- package/src/core/preflight.js +19 -2
- package/src/core/prepared-execution.js +227 -0
- package/src/core/progress.js +41 -4
- package/src/core/project-root.js +21 -0
- package/src/core/protocol-info.js +61 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +35 -10
- package/src/core/recovery-history.js +116 -0
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +9 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-claim-state.js +272 -0
- package/src/core/task-command.js +5 -1
- package/src/core/task-conflict-inspection.js +321 -0
- package/src/core/task-context.js +32 -29
- package/src/core/task-discovery.js +14 -1
- package/src/core/task-lock.js +216 -22
- package/src/core/task-paths.js +31 -2
- package/src/core/task-recovery-migration.js +192 -0
- package/src/core/task-recovery.js +205 -0
- package/src/core/task-scope.js +33 -1
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +9 -0
- package/src/core/trace.js +548 -0
- package/src/core/trajectory-evaluation.js +71 -0
- package/src/core/trajectory-metrics.js +80 -0
- package/src/core/transaction.js +36 -2
- package/src/core/work-state.js +10 -5
- package/src/integration.js +47 -0
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
package/src/core/protocol.js
CHANGED
|
@@ -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
|
]);
|
package/src/core/receipt.js
CHANGED
|
@@ -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
|
|
25
|
-
* already satisfied in the current repository but whose
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
(
|
|
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 };
|
package/src/core/resumability.js
CHANGED
|
@@ -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:
|
|
70
|
+
phase: resumedPhase,
|
|
17
71
|
selectedGuides: route.value.guides,
|
|
18
|
-
completedSteps:
|
|
19
|
-
pendingSteps:
|
|
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 {
|