@cassiomc1/forgeloop 1.2.4 → 1.5.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/AGENT_COMPATIBILITY.md +4 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +14 -0
- package/EXECUTION_STATE.md +48 -0
- package/LOOP_ENGINEERING.md +55 -6
- package/LOOP_SYSTEM_DESIGN.md +32 -1
- package/PROTOCOL_INTEGRATION.md +71 -0
- package/README.md +86 -0
- package/TERMINOLOGY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/THREAT_MODEL.md +22 -1
- package/docs/ARTIFACT_REFERENCE.md +54 -0
- package/docs/CLI_REFERENCE.md +177 -5
- package/docs/CROSS_HARNESS_CONTINUITY.md +34 -0
- package/docs/DOCUMENTATION_GUIDE.md +31 -0
- package/docs/GETTING_STARTED.md +10 -0
- package/docs/MCP.md +126 -0
- package/docs/RECIPES.md +87 -1
- package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
- package/docs/TROUBLESHOOTING.md +243 -40
- package/docs/UNIVERSAL_INTEGRATION.md +48 -0
- package/package.json +17 -3
- package/schemas/execution.schema.json +11 -1
- package/schemas/task-recovery.schema.json +61 -0
- package/schemas/work-state.schema.json +1 -0
- package/src/cli.js +182 -337
- package/src/commands/audit.js +5 -0
- package/src/commands/doctor.js +22 -0
- package/src/commands/inspect.js +6 -0
- package/src/commands/migrate-protocol.js +18 -0
- package/src/commands/progress.js +6 -2
- package/src/commands/protocol-info.js +16 -0
- package/src/commands/run-check.js +2 -0
- package/src/commands/status.js +17 -0
- package/src/commands/task-create.js +42 -3
- package/src/commands/task-list.js +14 -1
- package/src/commands/task-lock-status.js +29 -0
- 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 +21 -7
- package/src/commands/task-unlock.js +8 -6
- package/src/commands/validate-protocol.js +19 -2
- package/src/core/artifact-registry.js +12 -0
- package/src/core/artifacts.js +17 -4
- package/src/core/audit.js +20 -4
- package/src/core/bundles.js +15 -0
- package/src/core/cli-command-definitions.js +94 -4
- package/src/core/command-executors.js +387 -0
- package/src/core/command-input.js +107 -0
- package/src/core/command-runtime.js +106 -0
- package/src/core/completion-artifacts.js +17 -6
- package/src/core/completion-ownership.js +88 -0
- package/src/core/completion.js +15 -3
- package/src/core/diagnosis.js +15 -11
- package/src/core/error-codes.js +136 -1
- package/src/core/events.js +239 -9
- package/src/core/execution.js +73 -9
- package/src/core/filesystem.js +75 -8
- package/src/core/inspect.js +27 -0
- package/src/core/integration-invocation-policy.js +170 -0
- package/src/core/integration-limits.js +20 -0
- package/src/core/integration-resources.js +127 -0
- package/src/core/next-action-model.js +60 -0
- package/src/core/next-action.js +31 -0
- package/src/core/phase.js +10 -3
- package/src/core/project-root.js +21 -0
- package/src/core/protocol-info.js +54 -0
- package/src/core/protocol-migration.js +59 -0
- package/src/core/reconcile-closure.js +54 -14
- package/src/core/recovery-history.js +116 -0
- package/src/core/resumability.js +8 -6
- package/src/core/schema-validation.js +1 -0
- package/src/core/task-claim-state.js +272 -0
- package/src/core/task-command.js +8 -4
- 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 +248 -18
- package/src/core/task-migration.js +24 -1
- package/src/core/task-paths.js +6 -3
- 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/templates.js +1 -0
- package/src/core/transaction.js +285 -0
- package/src/core/work-state.js +70 -6
- package/src/integration.js +47 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { readContract } from "./contract.js";
|
|
2
|
-
import {
|
|
2
|
+
import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
|
+
import { appendProtocolEvent, validateEventLedger, readEvents, validateCompletionRecoveryAuthorization } from "./events.js";
|
|
3
4
|
import { runCommandExecution } from "./execution.js";
|
|
5
|
+
import { createReceipt } from "./receipt.js";
|
|
4
6
|
import { currentRepositoryFingerprint } from "./repository.js";
|
|
5
7
|
import { taskArtifactPath } from "./task-paths.js";
|
|
6
|
-
import { classifyLoadedWorkState, readWorkState,
|
|
8
|
+
import { classifyLoadedWorkState, readWorkState, mutateWorkState } from "./work-state.js";
|
|
7
9
|
|
|
8
10
|
export const RECONCILE_EVENT = "CHECKPOINT_RECONCILED";
|
|
9
11
|
|
|
10
12
|
const RECONCILABLE_DRIFT = new Set(["REPOSITORY_CHANGED"]);
|
|
11
13
|
|
|
12
|
-
const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING"]);
|
|
14
|
+
const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING", "REVIEWING"]);
|
|
13
15
|
|
|
14
16
|
function reconcileError(code, message, artifacts = []) {
|
|
15
17
|
const error = new Error(message);
|
|
@@ -19,12 +21,13 @@ function reconcileError(code, message, artifacts = []) {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
|
-
* Canonical recovery for an EXECUTING or
|
|
23
|
-
* already satisfied in the current repository but whose
|
|
24
|
-
* checkpoint is stale because the repository fingerprint moved.
|
|
24
|
+
* Canonical recovery for an EXECUTING, VERIFYING, or REVIEWING task whose
|
|
25
|
+
* objective is already satisfied in the current repository but whose
|
|
26
|
+
* work-state checkpoint is stale because the repository fingerprint moved.
|
|
25
27
|
*
|
|
26
28
|
* The command refreshes the checkpoint repository fingerprint only after:
|
|
27
|
-
* - the task is EXECUTING or
|
|
29
|
+
* - the task is EXECUTING, VERIFYING, or (with authorized completion
|
|
30
|
+
* recovery) REVIEWING,
|
|
28
31
|
* - classification requires revalidation and the only drift is
|
|
29
32
|
* REPOSITORY_CHANGED,
|
|
30
33
|
* - the append-only event ledger is valid,
|
|
@@ -63,6 +66,7 @@ export async function runReconcileClosure({
|
|
|
63
66
|
const stateRel = taskArtifactPath(taskId, "state");
|
|
64
67
|
const contractRel = taskArtifactPath(taskId, "contract");
|
|
65
68
|
const eventsRel = taskArtifactPath(taskId, "events");
|
|
69
|
+
const receiptRel = taskArtifactPath(taskId, "receipt");
|
|
66
70
|
|
|
67
71
|
const state = await readWorkState(target, { packageRoot, taskId });
|
|
68
72
|
if (!state) {
|
|
@@ -71,10 +75,28 @@ export async function runReconcileClosure({
|
|
|
71
75
|
if (!RECONCILABLE_PHASES.has(state.phase)) {
|
|
72
76
|
throw reconcileError(
|
|
73
77
|
"E_RECONCILE_PHASE_INVALID",
|
|
74
|
-
`reconcile-closure supports EXECUTING or
|
|
78
|
+
`reconcile-closure supports EXECUTING, VERIFYING, or REVIEWING tasks whose objective is already satisfied; found ${state.phase}`,
|
|
75
79
|
[stateRel],
|
|
76
80
|
);
|
|
77
81
|
}
|
|
82
|
+
if (state.phase === "REVIEWING") {
|
|
83
|
+
let receipt = null;
|
|
84
|
+
try {
|
|
85
|
+
receipt = (await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot))?.value ?? null;
|
|
86
|
+
} catch {
|
|
87
|
+
receipt = null;
|
|
88
|
+
}
|
|
89
|
+
const events = await readEvents(target, packageRoot, { taskId });
|
|
90
|
+
const recoveryAuth = validateCompletionRecoveryAuthorization({ state, receipt, events });
|
|
91
|
+
if (!recoveryAuth.authorized) {
|
|
92
|
+
const first = 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
|
+
}
|
|
78
100
|
|
|
79
101
|
const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
|
|
80
102
|
if (freshness.status !== "REVALIDATION_REQUIRED" || !freshness.reasons.includes("REPOSITORY_CHANGED")) {
|
|
@@ -104,9 +126,12 @@ export async function runReconcileClosure({
|
|
|
104
126
|
}
|
|
105
127
|
|
|
106
128
|
const contract = await readContract(target, packageRoot, { taskId });
|
|
107
|
-
const verificationItem = (contract.value.verification ?? []).find(
|
|
108
|
-
(
|
|
109
|
-
|
|
129
|
+
const verificationItem = (contract.value.verification ?? []).find((item) => {
|
|
130
|
+
if (typeof item === "string") {
|
|
131
|
+
return item === requirement;
|
|
132
|
+
}
|
|
133
|
+
return item.type === "VERIFICATION" && item.id === checkId && item.text === requirement;
|
|
134
|
+
});
|
|
110
135
|
if (!verificationItem) {
|
|
111
136
|
throw reconcileError(
|
|
112
137
|
"E_RECONCILE_REQUIREMENT_UNKNOWN",
|
|
@@ -154,11 +179,26 @@ export async function runReconcileClosure({
|
|
|
154
179
|
},
|
|
155
180
|
}, packageRoot, { taskId });
|
|
156
181
|
|
|
157
|
-
await
|
|
182
|
+
const nextState = await mutateWorkState(target, {
|
|
183
|
+
expectedRevision: state.revision ?? 0,
|
|
184
|
+
packageRoot,
|
|
185
|
+
taskId,
|
|
186
|
+
}, () => ({
|
|
158
187
|
...state,
|
|
159
188
|
repositoryFingerprint: repository,
|
|
160
189
|
lastUpdated: new Date().toISOString(),
|
|
161
|
-
}
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const receipt = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
|
|
194
|
+
const reboundReceipt = await createReceipt({
|
|
195
|
+
...receipt.value,
|
|
196
|
+
stateFingerprint: canonicalFingerprint(nextState),
|
|
197
|
+
}, packageRoot, { target, taskId, authorityContext, runtimeContext });
|
|
198
|
+
await writeJsonArtifact(target, receiptRel, reboundReceipt, "execution-receipt", packageRoot);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
201
|
+
}
|
|
162
202
|
|
|
163
203
|
return {
|
|
164
204
|
taskId,
|
|
@@ -172,4 +212,4 @@ export async function runReconcileClosure({
|
|
|
172
212
|
executionPath: execution.path,
|
|
173
213
|
event: RECONCILE_EVENT,
|
|
174
214
|
};
|
|
175
|
-
}
|
|
215
|
+
}
|
|
@@ -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
|
+
}
|
package/src/core/resumability.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { currentRepositoryFingerprint } from "./repository.js";
|
|
2
|
-
import { createWorkState, readWorkState,
|
|
2
|
+
import { createWorkState, initializeWorkState, readWorkState, mutateWorkState } from "./work-state.js";
|
|
3
3
|
|
|
4
4
|
const DEFAULT_PENDING_STEPS = ["planning", "implementation", "verification"];
|
|
5
5
|
|
|
@@ -22,8 +22,7 @@ export async function ensureResumableState({ target, packageRoot, contract, rout
|
|
|
22
22
|
blockers: [],
|
|
23
23
|
verificationEvidence: [],
|
|
24
24
|
});
|
|
25
|
-
|
|
26
|
-
return state;
|
|
25
|
+
return initializeWorkState(target, state, { packageRoot, taskId, statePath });
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
export async function synchronizePreflightState({
|
|
@@ -57,7 +56,10 @@ export async function synchronizePreflightState({
|
|
|
57
56
|
if (JSON.stringify(withoutTimestamp(candidate)) === JSON.stringify(withoutTimestamp(state))) {
|
|
58
57
|
return state;
|
|
59
58
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
return mutateWorkState(target, {
|
|
60
|
+
expectedRevision: state.revision ?? 0,
|
|
61
|
+
packageRoot,
|
|
62
|
+
taskId,
|
|
63
|
+
statePath,
|
|
64
|
+
}, () => ({ ...candidate, lastUpdated: new Date().toISOString() }));
|
|
63
65
|
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
E_COMPLETION_OWNERSHIP_UNPROVEN,
|
|
3
|
+
E_TASK_CLAIM_OWNERSHIP_INCONSISTENT,
|
|
4
|
+
E_TASK_COMPLETE,
|
|
5
|
+
E_TASK_RECOVERED,
|
|
6
|
+
E_TASK_RECOVERY_INCONSISTENT,
|
|
7
|
+
} from "./error-codes.js";
|
|
8
|
+
import { validateCompletionOwnershipProof } from "./completion-ownership.js";
|
|
9
|
+
import { validateEventLedger } from "./events.js";
|
|
10
|
+
import { classifyRecoveryHistory } from "./recovery-history.js";
|
|
11
|
+
import { readTaskDescriptor } from "./task-descriptor.js";
|
|
12
|
+
import { normalizeWriteClaims } from "./task-scope.js";
|
|
13
|
+
import {
|
|
14
|
+
readTaskRecovery,
|
|
15
|
+
validateTaskRecoveryConsistency,
|
|
16
|
+
} from "./task-recovery.js";
|
|
17
|
+
import { readWorkState } from "./work-state.js";
|
|
18
|
+
|
|
19
|
+
function ownershipError(message, cause = null) {
|
|
20
|
+
return {
|
|
21
|
+
code: E_TASK_RECOVERY_INCONSISTENT,
|
|
22
|
+
message,
|
|
23
|
+
...(cause?.code ? { causeCode: cause.code } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function appendClaims(target, claims, errors, source) {
|
|
28
|
+
if (claims === undefined || claims === null) return;
|
|
29
|
+
try {
|
|
30
|
+
target.push(...normalizeWriteClaims(claims));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
errors.push(ownershipError(`Invalid ${source} write claims: ${error.message}`, error));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Collects every input needed to resolve claim ownership from one immutable
|
|
38
|
+
* snapshot: descriptor, work state, recovery tombstone, validated ledger, and
|
|
39
|
+
* classified recovery history. Classification is a pure function of this
|
|
40
|
+
* evidence, so conflict inspection can reuse it without rereading artifacts.
|
|
41
|
+
*/
|
|
42
|
+
export async function collectTaskClaimEvidence(target, {
|
|
43
|
+
taskId,
|
|
44
|
+
packageRoot,
|
|
45
|
+
descriptor: suppliedDescriptor = null,
|
|
46
|
+
state: suppliedState = null,
|
|
47
|
+
} = {}) {
|
|
48
|
+
const errors = [];
|
|
49
|
+
let descriptor = suppliedDescriptor;
|
|
50
|
+
let state = suppliedState;
|
|
51
|
+
let recovery = null;
|
|
52
|
+
let ledger = { valid: false, events: [], errors: [] };
|
|
53
|
+
|
|
54
|
+
if (!descriptor) {
|
|
55
|
+
try {
|
|
56
|
+
descriptor = (await readTaskDescriptor(target, taskId, packageRoot)).value;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
errors.push(ownershipError(`Task descriptor cannot establish claim ownership: ${error.message}`, error));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!state) {
|
|
62
|
+
try {
|
|
63
|
+
state = await readWorkState(target, { taskId, packageRoot });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
errors.push(ownershipError(`Task work state cannot establish claim ownership: ${error.message}`, error));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
recovery = (await readTaskRecovery(target, { taskId, packageRoot }))?.value ?? null;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
errors.push(ownershipError(`Task recovery artifact cannot establish claim ownership: ${error.message}`, error));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
ledger = await validateEventLedger(target, packageRoot, { taskId });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
errors.push(ownershipError(`Task event ledger cannot establish claim ownership: ${error.message}`, error));
|
|
79
|
+
}
|
|
80
|
+
if (!ledger.valid) {
|
|
81
|
+
for (const error of ledger.errors) {
|
|
82
|
+
errors.push(ownershipError(`Task event ledger is invalid: ${error.message}`, error));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (ledger.events.some((event) => event.taskId !== taskId)) {
|
|
86
|
+
errors.push(ownershipError(`Task event ledger contains an event for a different task`));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const history = classifyRecoveryHistory(ledger.events);
|
|
90
|
+
const descriptorClaims = [];
|
|
91
|
+
appendClaims(descriptorClaims, descriptor?.writeClaims, errors, "descriptor");
|
|
92
|
+
const normalizedDescriptorClaims = normalizeWriteClaims(descriptorClaims);
|
|
93
|
+
|
|
94
|
+
const historicalClaims = [...normalizedDescriptorClaims];
|
|
95
|
+
for (const cycle of history.recoveries) {
|
|
96
|
+
appendClaims(historicalClaims, cycle.event?.details?.releasedClaims, errors, "recovery ledger");
|
|
97
|
+
}
|
|
98
|
+
appendClaims(historicalClaims, recovery?.releasedClaims, errors, "recovery artifact");
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
taskId,
|
|
102
|
+
phase: state?.phase ?? null,
|
|
103
|
+
descriptor,
|
|
104
|
+
state,
|
|
105
|
+
recovery,
|
|
106
|
+
ledger,
|
|
107
|
+
history,
|
|
108
|
+
normalizedDescriptorClaims,
|
|
109
|
+
historicalWriteClaims: normalizeWriteClaims(historicalClaims),
|
|
110
|
+
errors,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function inconsistentResult({ taskId, phase, historicalWriteClaims, recovery, errors }) {
|
|
115
|
+
const reasonCodes = [
|
|
116
|
+
E_TASK_CLAIM_OWNERSHIP_INCONSISTENT,
|
|
117
|
+
...errors.flatMap((error) => [error.code, error.causeCode]).filter(Boolean),
|
|
118
|
+
];
|
|
119
|
+
return {
|
|
120
|
+
taskId,
|
|
121
|
+
phase,
|
|
122
|
+
historicalWriteClaims,
|
|
123
|
+
effectiveWriteClaims: historicalWriteClaims,
|
|
124
|
+
writeClaims: historicalWriteClaims,
|
|
125
|
+
claimState: "INCONSISTENT",
|
|
126
|
+
mutationAllowed: false,
|
|
127
|
+
recovery,
|
|
128
|
+
recoveryStatus: "INCONSISTENT",
|
|
129
|
+
valid: false,
|
|
130
|
+
ownershipValid: false,
|
|
131
|
+
reasonCodes: [...new Set(reasonCodes)],
|
|
132
|
+
errors,
|
|
133
|
+
ownershipErrors: errors,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Deterministic claim-ownership decision table over collected evidence.
|
|
139
|
+
* Claims are released only for validated canonical completion or validated
|
|
140
|
+
* active recovery; everything else fails closed.
|
|
141
|
+
*/
|
|
142
|
+
export function classifyTaskClaimState(evidence) {
|
|
143
|
+
const {
|
|
144
|
+
taskId,
|
|
145
|
+
phase,
|
|
146
|
+
recovery,
|
|
147
|
+
ledger,
|
|
148
|
+
history,
|
|
149
|
+
normalizedDescriptorClaims,
|
|
150
|
+
historicalWriteClaims,
|
|
151
|
+
} = evidence;
|
|
152
|
+
|
|
153
|
+
// Classification is side-effect free: never mutate the supplied evidence.
|
|
154
|
+
const errors = [...(evidence.errors ?? [])];
|
|
155
|
+
errors.push(...validateTaskRecoveryConsistency({
|
|
156
|
+
taskId,
|
|
157
|
+
recovery,
|
|
158
|
+
events: ledger.events,
|
|
159
|
+
historicalWriteClaims: normalizedDescriptorClaims,
|
|
160
|
+
recoveryHistory: history,
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
if (errors.length > 0) {
|
|
164
|
+
return inconsistentResult({ taskId, phase, historicalWriteClaims, recovery, errors });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (phase === "COMPLETE") {
|
|
168
|
+
const proof = validateCompletionOwnershipProof({ taskId, state: evidence.state, ledger });
|
|
169
|
+
if (!proof.valid) {
|
|
170
|
+
return inconsistentResult({
|
|
171
|
+
taskId,
|
|
172
|
+
phase,
|
|
173
|
+
historicalWriteClaims,
|
|
174
|
+
recovery,
|
|
175
|
+
errors: [
|
|
176
|
+
ownershipError(
|
|
177
|
+
"COMPLETE work-state lacks canonical lifecycle/ledger completion proof; claims stay reserved",
|
|
178
|
+
{ code: E_COMPLETION_OWNERSHIP_UNPROVEN },
|
|
179
|
+
),
|
|
180
|
+
...proof.errors.map((error) => ownershipError(error.message, { code: error.code })),
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
taskId,
|
|
186
|
+
phase,
|
|
187
|
+
historicalWriteClaims,
|
|
188
|
+
effectiveWriteClaims: [],
|
|
189
|
+
writeClaims: [],
|
|
190
|
+
claimState: "RELEASED_BY_COMPLETION",
|
|
191
|
+
mutationAllowed: false,
|
|
192
|
+
recovery: null,
|
|
193
|
+
recoveryStatus: "NOT_APPLICABLE",
|
|
194
|
+
valid: true,
|
|
195
|
+
ownershipValid: true,
|
|
196
|
+
reasonCodes: [],
|
|
197
|
+
errors: [],
|
|
198
|
+
ownershipErrors: [],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (history.activeRecovery) {
|
|
203
|
+
// The consistency validator above guarantees that the active ledger cycle
|
|
204
|
+
// and recovery artifact identify the same recovery before this branch.
|
|
205
|
+
return {
|
|
206
|
+
taskId,
|
|
207
|
+
phase,
|
|
208
|
+
historicalWriteClaims,
|
|
209
|
+
effectiveWriteClaims: [],
|
|
210
|
+
writeClaims: [],
|
|
211
|
+
claimState: "RELEASED_BY_RECOVERY",
|
|
212
|
+
mutationAllowed: false,
|
|
213
|
+
recovery,
|
|
214
|
+
recoveryStatus: "ACTIVE",
|
|
215
|
+
valid: true,
|
|
216
|
+
ownershipValid: true,
|
|
217
|
+
reasonCodes: [],
|
|
218
|
+
errors: [],
|
|
219
|
+
ownershipErrors: [],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
taskId,
|
|
225
|
+
phase,
|
|
226
|
+
historicalWriteClaims,
|
|
227
|
+
effectiveWriteClaims: normalizedDescriptorClaims,
|
|
228
|
+
writeClaims: normalizedDescriptorClaims,
|
|
229
|
+
claimState: "ACTIVE",
|
|
230
|
+
mutationAllowed: true,
|
|
231
|
+
recovery: null,
|
|
232
|
+
recoveryStatus: history.completedRecoveries.length > 0 ? "COMPLETED" : "ABSENT",
|
|
233
|
+
valid: true,
|
|
234
|
+
ownershipValid: true,
|
|
235
|
+
reasonCodes: [],
|
|
236
|
+
errors: [],
|
|
237
|
+
ownershipErrors: [],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Resolve claim ownership from the task descriptor, work state, recovery
|
|
243
|
+
* tombstone, and the complete validated task ledger. Any disagreement retains
|
|
244
|
+
* every claim that can be recovered from validated inputs and fails closed.
|
|
245
|
+
*/
|
|
246
|
+
export async function resolveTaskClaimState(target, options = {}) {
|
|
247
|
+
const evidence = await collectTaskClaimEvidence(target, options);
|
|
248
|
+
return classifyTaskClaimState(evidence);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function assertTaskMutationAllowed(target, options = {}) {
|
|
252
|
+
const result = await resolveTaskClaimState(target, options);
|
|
253
|
+
if (result.mutationAllowed) return result;
|
|
254
|
+
|
|
255
|
+
const inconsistent = result.claimState === "INCONSISTENT";
|
|
256
|
+
const error = new Error(inconsistent
|
|
257
|
+
? `Task ${result.taskId} claim ownership is inconsistent; ordinary mutation is blocked`
|
|
258
|
+
: result.claimState === "RELEASED_BY_COMPLETION"
|
|
259
|
+
? `Task ${result.taskId} is COMPLETE and cannot be mutated`
|
|
260
|
+
: `Task ${result.taskId} is RECOVERED and its write claims are released; run task-resume before ordinary mutation`);
|
|
261
|
+
error.code = inconsistent
|
|
262
|
+
? E_TASK_CLAIM_OWNERSHIP_INCONSISTENT
|
|
263
|
+
: result.claimState === "RELEASED_BY_COMPLETION"
|
|
264
|
+
? E_TASK_COMPLETE
|
|
265
|
+
: E_TASK_RECOVERED;
|
|
266
|
+
error.taskId = result.taskId;
|
|
267
|
+
error.claimState = result.claimState;
|
|
268
|
+
error.reasonCodes = result.reasonCodes;
|
|
269
|
+
error.errors = result.errors;
|
|
270
|
+
error.recovery = result.recovery;
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
package/src/core/task-command.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { resolveTaskContext } from "./task-context.js";
|
|
2
|
-
import {
|
|
1
|
+
import { resolveTaskContext, TASK_SELECTION_MODES } from "./task-context.js";
|
|
2
|
+
import { withTaskTransaction } from "./transaction.js";
|
|
3
|
+
import { assertTaskMutationAllowed } from "./task-claim-state.js";
|
|
3
4
|
|
|
4
5
|
export async function withResolvedTask(
|
|
5
6
|
target,
|
|
@@ -12,6 +13,7 @@ export async function withResolvedTask(
|
|
|
12
13
|
taskId: taskOption,
|
|
13
14
|
explicitRequired,
|
|
14
15
|
packageRoot: options.packageRoot,
|
|
16
|
+
selectionMode: TASK_SELECTION_MODES.READ,
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
return callback(taskContext);
|
|
@@ -29,11 +31,13 @@ export async function withTaskMutation(
|
|
|
29
31
|
taskId: taskOption,
|
|
30
32
|
explicitRequired,
|
|
31
33
|
packageRoot: options.packageRoot,
|
|
34
|
+
selectionMode: TASK_SELECTION_MODES.MUTATION,
|
|
32
35
|
});
|
|
33
36
|
|
|
34
37
|
if (taskContext) {
|
|
35
|
-
return
|
|
36
|
-
|
|
38
|
+
return withTaskTransaction({ target, taskId: taskContext.taskId, operation, packageRoot: options.packageRoot, recordCommitEvent: true }, async (transaction) => {
|
|
39
|
+
await assertTaskMutationAllowed(target, { taskId: taskContext.taskId, packageRoot: options.packageRoot });
|
|
40
|
+
return callback({ ...taskContext, transaction });
|
|
37
41
|
});
|
|
38
42
|
}
|
|
39
43
|
|