@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
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { classifyLoadedWorkState, readWorkState } from "./work-state.js";
|
|
2
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
3
|
+
import { readLockInfo, classifyLockStaleness } from "./task-lock.js";
|
|
4
|
+
import { validateStateLedgerCoherence } from "./events.js";
|
|
5
|
+
import { currentRepositoryFingerprint } from "./repository.js";
|
|
6
|
+
import { collectTaskClaimEvidence, classifyTaskClaimState } from "./task-claim-state.js";
|
|
7
|
+
|
|
8
|
+
export const TASK_CONFLICT_CLASSIFICATIONS = Object.freeze([
|
|
9
|
+
"ACTIVE",
|
|
10
|
+
"RECOVERABLE",
|
|
11
|
+
"STALE",
|
|
12
|
+
"ABANDONED",
|
|
13
|
+
"INCONSISTENT",
|
|
14
|
+
"RECOVERED",
|
|
15
|
+
"COMPLETE",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const POST_EXECUTION_PHASES = new Set([
|
|
19
|
+
"EXECUTING",
|
|
20
|
+
"VERIFYING",
|
|
21
|
+
"DIAGNOSING",
|
|
22
|
+
"CORRECTING",
|
|
23
|
+
"REVIEWING",
|
|
24
|
+
"COMPLETE",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export const TASK_CONFLICT_IDLE_THRESHOLD_MS = 14 * 24 * 60 * 60 * 1000;
|
|
28
|
+
|
|
29
|
+
const RECONCILABLE_DRIFT = new Set(["REPOSITORY_CHANGED"]);
|
|
30
|
+
export const MEANINGFUL_ACTIVITY_EVENTS = new Set([
|
|
31
|
+
"EXECUTION_STARTED",
|
|
32
|
+
"VERIFICATION_STARTED",
|
|
33
|
+
"CHECK_RECORDED",
|
|
34
|
+
"VERIFICATION_RECORDED",
|
|
35
|
+
"DIAGNOSIS_RECORDED",
|
|
36
|
+
"CORRECTION_STARTED",
|
|
37
|
+
"REVIEW_STARTED",
|
|
38
|
+
"CONTINUITY_RECORDED",
|
|
39
|
+
"CHECKPOINT_RECONCILED",
|
|
40
|
+
"TASK_RECOVERY_RESUMED",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function idleMs(lastUpdated, now) {
|
|
44
|
+
const parsed = Date.parse(lastUpdated);
|
|
45
|
+
if (!Number.isFinite(parsed)) return null;
|
|
46
|
+
return Math.max(0, now - parsed);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function passedCheckCount(state) {
|
|
50
|
+
return (state?.checks ?? []).filter((check) => check.status === "passed").length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function checkCount(state, status) {
|
|
54
|
+
return (state?.checks ?? []).filter((check) => check.status === status).length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function lastMeaningfulActivity(state, events) {
|
|
58
|
+
const candidates = [
|
|
59
|
+
...(state?.lastUpdated ? [{ at: state.lastUpdated, type: "WORK_STATE_UPDATED" }] : []),
|
|
60
|
+
...events
|
|
61
|
+
.filter((event) => MEANINGFUL_ACTIVITY_EVENTS.has(event.event))
|
|
62
|
+
.map((event) => ({ at: event.at, type: event.event })),
|
|
63
|
+
].filter((candidate) => Number.isFinite(Date.parse(candidate.at)));
|
|
64
|
+
return candidates.sort((left, right) => Date.parse(right.at) - Date.parse(left.at))[0] ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Deterministic classification of a potentially conflicting task from
|
|
69
|
+
* machine-readable state only. REVIEWING plus an old timestamp alone is never
|
|
70
|
+
* sufficient for STALE or ABANDONED: post-execution tasks whose only drift is
|
|
71
|
+
* REPOSITORY_CHANGED remain RECOVERABLE because an official recovery path
|
|
72
|
+
* exists.
|
|
73
|
+
*/
|
|
74
|
+
export function classifyConflictEvidence(evidence, {
|
|
75
|
+
now = Date.now(),
|
|
76
|
+
idleThresholdMs = TASK_CONFLICT_IDLE_THRESHOLD_MS,
|
|
77
|
+
} = {}) {
|
|
78
|
+
if (!evidence || evidence.healthy === false) {
|
|
79
|
+
return {
|
|
80
|
+
classification: "INCONSISTENT",
|
|
81
|
+
reasonCodes: evidence?.healthReasonCodes?.length > 0
|
|
82
|
+
? evidence.healthReasonCodes
|
|
83
|
+
: ["E_TASK_DESCRIPTOR_INVALID"],
|
|
84
|
+
recoverable: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const {
|
|
89
|
+
lockStatus,
|
|
90
|
+
freshnessStatus,
|
|
91
|
+
freshnessReasons,
|
|
92
|
+
phase,
|
|
93
|
+
lastUpdated,
|
|
94
|
+
lastMeaningfulActivityAt,
|
|
95
|
+
ledgerValid,
|
|
96
|
+
recordedChecks,
|
|
97
|
+
totalChecks,
|
|
98
|
+
verificationEvidenceCount,
|
|
99
|
+
recoveryStatus,
|
|
100
|
+
} = evidence;
|
|
101
|
+
|
|
102
|
+
if (!ledgerValid) {
|
|
103
|
+
return {
|
|
104
|
+
classification: "INCONSISTENT",
|
|
105
|
+
reasonCodes: ["E_LEDGER_INVALID"],
|
|
106
|
+
recoverable: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const KNOWN_PHASES = new Set([
|
|
110
|
+
"RECEIVED", "DISCOVERING", "CONTRACT_READY", "ROUTED", "DESIGNING", "PLANNED",
|
|
111
|
+
...POST_EXECUTION_PHASES,
|
|
112
|
+
]);
|
|
113
|
+
if (!phase || !KNOWN_PHASES.has(phase)) {
|
|
114
|
+
return {
|
|
115
|
+
classification: "INCONSISTENT",
|
|
116
|
+
reasonCodes: ["E_PHASE_UNKNOWN"],
|
|
117
|
+
recoverable: false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if (lockStatus === "LIVE") {
|
|
123
|
+
return {
|
|
124
|
+
classification: "ACTIVE",
|
|
125
|
+
reasonCodes: ["E_TASK_LOCKED"],
|
|
126
|
+
recoverable: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (lockStatus === "UNKNOWN" || lockStatus === "CORRUPT") {
|
|
131
|
+
return {
|
|
132
|
+
classification: "INCONSISTENT",
|
|
133
|
+
reasonCodes: ["E_TASK_LOCK_STATE_UNKNOWN"],
|
|
134
|
+
recoverable: false,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (freshnessStatus === "UNKNOWN") {
|
|
139
|
+
return {
|
|
140
|
+
classification: "INCONSISTENT",
|
|
141
|
+
reasonCodes: freshnessReasons?.length > 0 ? freshnessReasons : ["E_STATE_UNREADABLE"],
|
|
142
|
+
recoverable: false,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (recoveryStatus === "RECOVERED") {
|
|
147
|
+
return {
|
|
148
|
+
classification: "RECOVERED",
|
|
149
|
+
reasonCodes: ["TASK_RECOVERED"],
|
|
150
|
+
recoverable: false,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (phase === "COMPLETE") {
|
|
155
|
+
return {
|
|
156
|
+
classification: "COMPLETE",
|
|
157
|
+
reasonCodes: ["TASK_COMPLETE"],
|
|
158
|
+
recoverable: false,
|
|
159
|
+
terminal: true,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const drift = freshnessReasons ?? [];
|
|
164
|
+
const staleOnlyRepositoryDrift = freshnessStatus === "REVALIDATION_REQUIRED"
|
|
165
|
+
&& drift.length > 0
|
|
166
|
+
&& drift.every((reason) => RECONCILABLE_DRIFT.has(reason));
|
|
167
|
+
const idle = idleMs(lastMeaningfulActivityAt ?? lastUpdated, now);
|
|
168
|
+
const idleBeyondThreshold = idle !== null && idle > idleThresholdMs;
|
|
169
|
+
|
|
170
|
+
if (freshnessStatus === "FRESH") {
|
|
171
|
+
return {
|
|
172
|
+
classification: "ACTIVE",
|
|
173
|
+
reasonCodes: ["STATE_FRESH"],
|
|
174
|
+
recoverable: false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (POST_EXECUTION_PHASES.has(phase)) {
|
|
179
|
+
if (staleOnlyRepositoryDrift) {
|
|
180
|
+
return {
|
|
181
|
+
classification: "RECOVERABLE",
|
|
182
|
+
reasonCodes: ["E_REPOSITORY_CHANGED", "OFFICIAL_RECOVERY_AVAILABLE"],
|
|
183
|
+
recoverable: true,
|
|
184
|
+
recoveredBy: recordedChecks > 0
|
|
185
|
+
? ["forgeloop reconcile-closure", "canonical VERIFYING -> REVIEWING -> complete pipeline"]
|
|
186
|
+
: ["forgeloop reconcile-closure", "canonical verification recording pipeline"],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const recordedEvidence = (totalChecks ?? recordedChecks ?? 0) + (verificationEvidenceCount ?? 0);
|
|
190
|
+
if (idleBeyondThreshold && recordedEvidence === 0) {
|
|
191
|
+
return {
|
|
192
|
+
classification: "ABANDONED",
|
|
193
|
+
reasonCodes: ["NO_RECORDED_EVIDENCE", "IDLE_BEYOND_THRESHOLD", ...drift],
|
|
194
|
+
recoverable: false,
|
|
195
|
+
recoveredBy: ["forgeloop task-recover --task <id> --acknowledge-recovery"],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
classification: "ACTIVE",
|
|
200
|
+
reasonCodes: ["RECENT_PROGRESS", ...(drift.length > 0 ? drift : [])],
|
|
201
|
+
recoverable: false,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (idleBeyondThreshold) {
|
|
206
|
+
return {
|
|
207
|
+
classification: "STALE",
|
|
208
|
+
reasonCodes: ["IDLE_BEYOND_THRESHOLD", ...(drift.length > 0 ? drift : ["STATE_FRESH"])],
|
|
209
|
+
recoverable: false,
|
|
210
|
+
recoveredBy: ["forgeloop task-recover --task <id> --acknowledge-recovery"],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
classification: "ACTIVE",
|
|
216
|
+
reasonCodes: ["RECENT_PROGRESS"],
|
|
217
|
+
recoverable: false,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function inspectTaskConflictState(target, {
|
|
222
|
+
taskId,
|
|
223
|
+
packageRoot,
|
|
224
|
+
now = Date.now(),
|
|
225
|
+
ignoredLockId = null,
|
|
226
|
+
} = {}) {
|
|
227
|
+
const [lockInfo, stateResult, ownershipEvidence] = await Promise.all([
|
|
228
|
+
readLockInfo(target, taskId),
|
|
229
|
+
readWorkState(target, { packageRoot, taskId })
|
|
230
|
+
.then((value) => ({ value, error: null }))
|
|
231
|
+
.catch((error) => ({ value: null, error })),
|
|
232
|
+
collectTaskClaimEvidence(target, { packageRoot, taskId }),
|
|
233
|
+
]);
|
|
234
|
+
const claimProjection = classifyTaskClaimState(ownershipEvidence);
|
|
235
|
+
const state = stateResult.value;
|
|
236
|
+
const recovery = claimProjection.recovery;
|
|
237
|
+
|
|
238
|
+
const lockClassification = ignoredLockId && lockInfo?.lockId === ignoredLockId
|
|
239
|
+
? { status: "NONE", stale: false }
|
|
240
|
+
: classifyLockStaleness(lockInfo, now);
|
|
241
|
+
|
|
242
|
+
let freshness = null;
|
|
243
|
+
if (state) {
|
|
244
|
+
try {
|
|
245
|
+
freshness = await classifyLoadedWorkState({
|
|
246
|
+
target,
|
|
247
|
+
state,
|
|
248
|
+
contractFile: taskArtifactPath(taskId, "contract"),
|
|
249
|
+
});
|
|
250
|
+
} catch {
|
|
251
|
+
freshness = { status: "UNKNOWN", reasons: ["E_STATE_UNREADABLE"] };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Reuse the ownership snapshot's already-validated ledger; no second full
|
|
256
|
+
// ledger parse inside this immutable inspection.
|
|
257
|
+
const coherenceErrors = ownershipEvidence.state && ownershipEvidence.ledger
|
|
258
|
+
? validateStateLedgerCoherence(ownershipEvidence.state, ownershipEvidence.ledger.events)
|
|
259
|
+
: [];
|
|
260
|
+
const ledgerValid = ownershipEvidence.ledger.valid && coherenceErrors.length === 0;
|
|
261
|
+
const ledgerEvents = ownershipEvidence.ledger.events;
|
|
262
|
+
const ledgerErrors = [...ownershipEvidence.ledger.errors, ...coherenceErrors];
|
|
263
|
+
|
|
264
|
+
const repository = await currentRepositoryFingerprint(target);
|
|
265
|
+
const meaningfulActivity = lastMeaningfulActivity(state, ledgerEvents);
|
|
266
|
+
const recoveryConsistencyErrors = claimProjection.ownershipErrors ?? claimProjection.errors ?? [];
|
|
267
|
+
const healthReasonCodes = [
|
|
268
|
+
...(stateResult.error ? ["E_STATE_UNREADABLE"] : []),
|
|
269
|
+
...(claimProjection.ownershipValid === false
|
|
270
|
+
? claimProjection.reasonCodes
|
|
271
|
+
: []),
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
const evidence = {
|
|
275
|
+
healthy: healthReasonCodes.length === 0,
|
|
276
|
+
healthReasonCodes,
|
|
277
|
+
descriptorHealthy: true,
|
|
278
|
+
phase: state?.phase ?? null,
|
|
279
|
+
lastUpdated: state?.lastUpdated ?? null,
|
|
280
|
+
lastMeaningfulActivityAt: meaningfulActivity?.at ?? null,
|
|
281
|
+
lastMeaningfulEventType: meaningfulActivity?.type ?? null,
|
|
282
|
+
workStateRevision: state?.revision ?? 0,
|
|
283
|
+
lockStatus: lockClassification.status,
|
|
284
|
+
lockId: lockInfo?.lockId ?? null,
|
|
285
|
+
lockExpiresAt: lockClassification.expiresAt ?? null,
|
|
286
|
+
freshnessStatus: freshness?.status ?? "UNKNOWN",
|
|
287
|
+
freshnessReasons: freshness?.reasons ?? [],
|
|
288
|
+
ledgerValid,
|
|
289
|
+
ledgerLastSeq: ledgerEvents.at(-1)?.seq ?? 0,
|
|
290
|
+
ledgerErrors,
|
|
291
|
+
recordedChecks: passedCheckCount(state),
|
|
292
|
+
passedCheckCount: passedCheckCount(state),
|
|
293
|
+
failedCheckCount: checkCount(state, "failed"),
|
|
294
|
+
blockedCheckCount: checkCount(state, "blocked"),
|
|
295
|
+
totalChecks: state?.checks?.length ?? 0,
|
|
296
|
+
verificationEvidenceCount: state?.verificationEvidence?.length ?? 0,
|
|
297
|
+
recoveryStatus: claimProjection.claimState === "RELEASED_BY_RECOVERY"
|
|
298
|
+
? "RECOVERED"
|
|
299
|
+
: recovery?.status ?? null,
|
|
300
|
+
recoveryConsistencyErrors,
|
|
301
|
+
claimState: claimProjection.claimState,
|
|
302
|
+
historicalWriteClaims: claimProjection.historicalWriteClaims,
|
|
303
|
+
effectiveWriteClaims: claimProjection.effectiveWriteClaims,
|
|
304
|
+
mutationAllowed: claimProjection.mutationAllowed,
|
|
305
|
+
ownershipValid: claimProjection.ownershipValid,
|
|
306
|
+
ownershipErrors: recoveryConsistencyErrors,
|
|
307
|
+
repositoryBranch: repository.branch,
|
|
308
|
+
repositoryHead: repository.head,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const verdict = classifyConflictEvidence(evidence, { now });
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
taskId,
|
|
315
|
+
classification: verdict.classification,
|
|
316
|
+
reasonCodes: verdict.reasonCodes,
|
|
317
|
+
recoverable: verdict.recoverable ?? false,
|
|
318
|
+
...(verdict.recoveredBy ? { recoveredBy: verdict.recoveredBy } : {}),
|
|
319
|
+
evidence,
|
|
320
|
+
};
|
|
321
|
+
}
|
package/src/core/task-context.js
CHANGED
|
@@ -27,11 +27,35 @@ export function createTaskContext({ target, taskId, descriptor = null, packageRo
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function isOperationallyActiveTask(task) {
|
|
31
|
+
return task?.healthy !== false
|
|
32
|
+
&& task?.phase !== "COMPLETE"
|
|
33
|
+
&& task?.mutationAllowed !== false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const TASK_SELECTION_MODES = Object.freeze({
|
|
37
|
+
READ: "READ",
|
|
38
|
+
MUTATION: "MUTATION",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* READ selection = task discoverability: any single healthy task may be
|
|
43
|
+
* inspected implicitly. MUTATION selection = task operational authority:
|
|
44
|
+
* only operationally active tasks are eligible implicit mutation targets.
|
|
45
|
+
*/
|
|
46
|
+
export function candidateTasksForSelection(tasks, selectionMode = TASK_SELECTION_MODES.MUTATION) {
|
|
47
|
+
if (selectionMode === TASK_SELECTION_MODES.READ) {
|
|
48
|
+
return tasks.filter((task) => task.healthy !== false);
|
|
49
|
+
}
|
|
50
|
+
return tasks.filter(isOperationallyActiveTask);
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
export async function resolveTaskContext(target, {
|
|
31
54
|
taskId = null,
|
|
32
55
|
envTaskId = process.env.FORGELOOP_TASK ?? null,
|
|
33
56
|
explicitRequired = false,
|
|
34
57
|
packageRoot = getPackageRoot(),
|
|
58
|
+
selectionMode = TASK_SELECTION_MODES.MUTATION,
|
|
35
59
|
} = {}) {
|
|
36
60
|
const flagId = typeof taskId === "string" && taskId.trim() ? taskId.trim() : null;
|
|
37
61
|
const envId = typeof envTaskId === "string" && envTaskId.trim() ? envTaskId.trim() : null;
|
|
@@ -82,8 +106,10 @@ export async function resolveTaskContext(target, {
|
|
|
82
106
|
throw error;
|
|
83
107
|
}
|
|
84
108
|
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
const candidateTasks = candidateTasksForSelection(healthyTasks, selectionMode);
|
|
110
|
+
|
|
111
|
+
if (candidateTasks.length === 1) {
|
|
112
|
+
const single = candidateTasks[0];
|
|
87
113
|
return createTaskContext({
|
|
88
114
|
target,
|
|
89
115
|
taskId: single.taskId,
|
|
@@ -92,33 +118,9 @@ export async function resolveTaskContext(target, {
|
|
|
92
118
|
});
|
|
93
119
|
}
|
|
94
120
|
|
|
95
|
-
if (
|
|
96
|
-
const activeTasks = healthyTasks.filter((t) => t.phase !== "COMPLETE");
|
|
97
|
-
|
|
98
|
-
// Exactly one active (non-COMPLETE) task -> select it implicitly
|
|
99
|
-
if (activeTasks.length === 1) {
|
|
100
|
-
const single = activeTasks[0];
|
|
101
|
-
return createTaskContext({
|
|
102
|
-
target,
|
|
103
|
-
taskId: single.taskId,
|
|
104
|
-
descriptor: single.descriptor,
|
|
105
|
-
packageRoot,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Zero active and exactly one total task -> select it
|
|
110
|
-
if (activeTasks.length === 0 && healthyTasks.length === 1) {
|
|
111
|
-
const single = healthyTasks[0];
|
|
112
|
-
return createTaskContext({
|
|
113
|
-
target,
|
|
114
|
-
taskId: single.taskId,
|
|
115
|
-
descriptor: single.descriptor,
|
|
116
|
-
packageRoot,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
121
|
+
if (candidateTasks.length > 1) {
|
|
120
122
|
// Otherwise ambiguous
|
|
121
|
-
const candidates =
|
|
123
|
+
const candidates = candidateTasks.map((t) => t.taskId);
|
|
122
124
|
const error = new Error(
|
|
123
125
|
`Multiple active tasks exist (${candidates.join(", ")}). Select a task with --task <id> or FORGELOOP_TASK=<id>.`,
|
|
124
126
|
);
|
|
@@ -127,9 +129,10 @@ export async function resolveTaskContext(target, {
|
|
|
127
129
|
throw error;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
if (explicitRequired) {
|
|
132
|
+
if (explicitRequired || healthyTasks.length > 0) {
|
|
131
133
|
const error = new Error("A task must be specified using --task <id> or created with 'forgeloop task-create'");
|
|
132
134
|
error.code = E_TASK_REQUIRED;
|
|
135
|
+
error.tasks = healthyTasks.map((task) => task.taskId);
|
|
133
136
|
throw error;
|
|
134
137
|
}
|
|
135
138
|
|
|
@@ -6,6 +6,7 @@ import { readTaskDescriptor } from "./task-descriptor.js";
|
|
|
6
6
|
import { readJsonArtifact } from "./artifacts.js";
|
|
7
7
|
import { readLockInfo } from "./task-lock.js";
|
|
8
8
|
import { taskStorageKey } from "./task-identity.js";
|
|
9
|
+
import { resolveTaskClaimState } from "./task-claim-state.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Explicitly recognized legacy-incidental artifacts that may legitimately
|
|
@@ -122,6 +123,14 @@ export async function discoverTasks(target, packageRoot = getPackageRoot()) {
|
|
|
122
123
|
const receiptPath = ensureWithin(target, taskArtifactPath(taskId, "receipt"));
|
|
123
124
|
const hasReceipt = await fileExists(receiptPath);
|
|
124
125
|
|
|
126
|
+
const claimProjection = await resolveTaskClaimState(target, {
|
|
127
|
+
taskId,
|
|
128
|
+
packageRoot,
|
|
129
|
+
descriptor,
|
|
130
|
+
state,
|
|
131
|
+
});
|
|
132
|
+
const recovery = claimProjection.recovery;
|
|
133
|
+
|
|
125
134
|
tasks.push({
|
|
126
135
|
taskId,
|
|
127
136
|
taskKey: descriptor.taskKey,
|
|
@@ -129,12 +138,16 @@ export async function discoverTasks(target, packageRoot = getPackageRoot()) {
|
|
|
129
138
|
phase,
|
|
130
139
|
locked: lockInfo !== null,
|
|
131
140
|
lockInfo,
|
|
132
|
-
|
|
141
|
+
...claimProjection,
|
|
142
|
+
ownershipValid: claimProjection.valid,
|
|
143
|
+
recovery,
|
|
144
|
+
operatorRecoveredAt: recovery?.recoveredAt ?? null,
|
|
133
145
|
createdAt: descriptor.createdAt,
|
|
134
146
|
updatedAt: descriptor.updatedAt,
|
|
135
147
|
lastUpdated,
|
|
136
148
|
hasContinuity,
|
|
137
149
|
hasReceipt,
|
|
150
|
+
...(claimProjection.errors.length > 0 ? { errors: claimProjection.errors } : {}),
|
|
138
151
|
descriptor,
|
|
139
152
|
directory: `${TASK_STATE_ROOT}/${descriptor.taskKey}`,
|
|
140
153
|
});
|