@cassiomc1/forgeloop 1.3.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/AGENTS.md +1 -0
  3. package/CLAUDE.md +1 -0
  4. package/DOCS_INDEX.md +20 -8
  5. package/EXECUTION_STATE.md +60 -0
  6. package/LOOP_ENGINEERING.md +135 -5
  7. package/LOOP_SYSTEM_DESIGN.md +54 -1
  8. package/PROTOCOL_INTEGRATION.md +87 -0
  9. package/QUALITY_SCORECARD.md +2 -0
  10. package/README.md +69 -9
  11. package/TERMINOLOGY.md +15 -0
  12. package/THIRD_PARTY_NOTICES.md +30 -0
  13. package/THREAT_MODEL.md +59 -1
  14. package/docs/ARTIFACT_REFERENCE.md +183 -0
  15. package/docs/CLI_REFERENCE.md +391 -6
  16. package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
  17. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  18. package/docs/DOCUMENTATION_GUIDE.md +36 -13
  19. package/docs/EXECUTION_TRACE.md +76 -0
  20. package/docs/GETTING_STARTED.md +1 -0
  21. package/docs/MCP.md +159 -0
  22. package/docs/RECIPES.md +149 -0
  23. package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
  24. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
  25. package/docs/TROUBLESHOOTING.md +217 -3
  26. package/docs/UNIVERSAL_INTEGRATION.md +48 -0
  27. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  28. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  29. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  30. package/docs/diagrams/README.md +55 -0
  31. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  32. package/docs/diagrams/manifest.json +42 -0
  33. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  34. package/package.json +21 -8
  35. package/schemas/action.schema.json +100 -0
  36. package/schemas/approval.schema.json +51 -0
  37. package/schemas/capability-policy.schema.json +41 -0
  38. package/schemas/diagnostic-case.schema.json +85 -0
  39. package/schemas/execution-receipt.schema.json +16 -0
  40. package/schemas/hypothesis-disposition.schema.json +16 -0
  41. package/schemas/intervention.schema.json +27 -0
  42. package/schemas/policy-lock.schema.json +1 -0
  43. package/schemas/policy-snapshot.schema.json +2 -0
  44. package/schemas/task-recovery.schema.json +61 -0
  45. package/schemas/trajectory-evaluation.schema.json +64 -0
  46. package/schemas/trajectory-scenario.schema.json +42 -0
  47. package/src/cli.js +267 -347
  48. package/src/commands/action-authorize.js +41 -0
  49. package/src/commands/action-propose.js +10 -0
  50. package/src/commands/action-reconcile.js +10 -0
  51. package/src/commands/action-record.js +47 -0
  52. package/src/commands/action-show.js +10 -0
  53. package/src/commands/action-verify.js +10 -0
  54. package/src/commands/advance.js +7 -2
  55. package/src/commands/approval-request.js +64 -0
  56. package/src/commands/approval-resolve.js +10 -0
  57. package/src/commands/audit.js +5 -0
  58. package/src/commands/baseline.js +3 -3
  59. package/src/commands/eval.js +6 -0
  60. package/src/commands/history.js +18 -0
  61. package/src/commands/init.js +2 -2
  62. package/src/commands/inspect.js +55 -0
  63. package/src/commands/metrics.js +7 -0
  64. package/src/commands/next.js +8 -2
  65. package/src/commands/policy-discover.js +2 -2
  66. package/src/commands/progress.js +6 -2
  67. package/src/commands/record-diagnosis.js +37 -1
  68. package/src/commands/record-hypothesis-disposition.js +45 -0
  69. package/src/commands/record-intervention.js +35 -0
  70. package/src/commands/reflect.js +38 -0
  71. package/src/commands/report.js +9 -1
  72. package/src/commands/run-action.js +18 -0
  73. package/src/commands/status.js +17 -0
  74. package/src/commands/task-create.js +39 -1
  75. package/src/commands/task-list.js +14 -1
  76. package/src/commands/task-lock-status.js +2 -2
  77. package/src/commands/task-recover.js +202 -0
  78. package/src/commands/task-repair-legacy-recovery.js +417 -0
  79. package/src/commands/task-resume.js +172 -0
  80. package/src/commands/task-scope.js +23 -4
  81. package/src/commands/task-show.js +18 -4
  82. package/src/commands/trace.js +34 -0
  83. package/src/commands/validate-protocol.js +40 -15
  84. package/src/core/action-authorization.js +106 -0
  85. package/src/core/action-constants.js +86 -0
  86. package/src/core/action-execution.js +105 -0
  87. package/src/core/action-ledger-projection.js +302 -0
  88. package/src/core/action-model.js +581 -0
  89. package/src/core/action-readiness.js +141 -0
  90. package/src/core/action-reconciliation-policy.js +49 -0
  91. package/src/core/action-reconciliation.js +66 -0
  92. package/src/core/action-verification.js +111 -0
  93. package/src/core/actions.js +462 -0
  94. package/src/core/approvals.js +405 -0
  95. package/src/core/artifact-registry.js +60 -0
  96. package/src/core/audit.js +45 -4
  97. package/src/core/bundles.js +30 -0
  98. package/src/core/capability-policy.js +226 -0
  99. package/src/core/cli-command-definitions.js +260 -5
  100. package/src/core/command-executors.js +543 -0
  101. package/src/core/command-input.js +107 -0
  102. package/src/core/command-runtime.js +117 -0
  103. package/src/core/completion-artifacts.js +39 -15
  104. package/src/core/completion-ownership.js +88 -0
  105. package/src/core/completion-recovery-rebind.js +194 -0
  106. package/src/core/completion.js +70 -0
  107. package/src/core/continuity-reconciliation.js +24 -5
  108. package/src/core/diagnostic-model.js +396 -0
  109. package/src/core/diagnostic-projection.js +51 -0
  110. package/src/core/diagnostic-record.js +360 -0
  111. package/src/core/error-codes.js +461 -1
  112. package/src/core/events.js +171 -2
  113. package/src/core/execution-prerequisites.js +4 -1
  114. package/src/core/execution.js +26 -188
  115. package/src/core/failure-signature.js +70 -0
  116. package/src/core/failure-surface.js +57 -0
  117. package/src/core/filesystem.js +55 -6
  118. package/src/core/history.js +110 -0
  119. package/src/core/hypothesis-projection.js +85 -0
  120. package/src/core/information-gain-projection.js +283 -0
  121. package/src/core/information-gain.js +138 -0
  122. package/src/core/inspect.js +132 -7
  123. package/src/core/integration-invocation-policy.js +217 -0
  124. package/src/core/integration-limits.js +20 -0
  125. package/src/core/integration-resources.js +178 -0
  126. package/src/core/next-action-model.js +94 -0
  127. package/src/core/next-action.js +490 -3
  128. package/src/core/phase.js +42 -22
  129. package/src/core/policy-engine.js +113 -6
  130. package/src/core/preflight-consistency.js +31 -5
  131. package/src/core/preflight.js +19 -2
  132. package/src/core/prepared-execution.js +227 -0
  133. package/src/core/progress.js +41 -4
  134. package/src/core/project-root.js +21 -0
  135. package/src/core/protocol-info.js +61 -0
  136. package/src/core/protocol.js +14 -0
  137. package/src/core/receipt.js +1 -0
  138. package/src/core/reconcile-closure.js +35 -10
  139. package/src/core/recovery-history.js +116 -0
  140. package/src/core/reflection.js +305 -0
  141. package/src/core/resumability.js +57 -3
  142. package/src/core/schema-validation.js +9 -0
  143. package/src/core/strategy-analysis.js +97 -0
  144. package/src/core/task-claim-state.js +272 -0
  145. package/src/core/task-command.js +5 -1
  146. package/src/core/task-conflict-inspection.js +321 -0
  147. package/src/core/task-context.js +32 -29
  148. package/src/core/task-discovery.js +14 -1
  149. package/src/core/task-lock.js +216 -22
  150. package/src/core/task-paths.js +31 -2
  151. package/src/core/task-recovery-migration.js +192 -0
  152. package/src/core/task-recovery.js +205 -0
  153. package/src/core/task-scope.js +33 -1
  154. package/src/core/task-snapshot.js +53 -0
  155. package/src/core/templates.js +9 -0
  156. package/src/core/trace.js +548 -0
  157. package/src/core/trajectory-evaluation.js +71 -0
  158. package/src/core/trajectory-metrics.js +80 -0
  159. package/src/core/transaction.js +36 -2
  160. package/src/core/work-state.js +10 -5
  161. package/src/integration.js +47 -0
  162. package/docs/assets/forgeloop-flow.svg +0 -1
  163. package/docs/forgeloop-flow.mmd +0 -51
@@ -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
+ }
@@ -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
- if (healthyTasks.length === 1) {
86
- const single = healthyTasks[0];
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 (healthyTasks.length > 1) {
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 = (activeTasks.length > 0 ? activeTasks : healthyTasks).map((t) => t.taskId);
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
- writeClaims: descriptor.writeClaims ?? [],
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
  });