@cassiomc1/forgeloop 1.3.0 → 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.
Files changed (76) 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 +7 -0
  5. package/EXECUTION_STATE.md +40 -0
  6. package/LOOP_ENGINEERING.md +54 -5
  7. package/LOOP_SYSTEM_DESIGN.md +22 -1
  8. package/PROTOCOL_INTEGRATION.md +41 -0
  9. package/README.md +38 -0
  10. package/TERMINOLOGY.md +15 -0
  11. package/THIRD_PARTY_NOTICES.md +15 -0
  12. package/THREAT_MODEL.md +20 -1
  13. package/docs/ARTIFACT_REFERENCE.md +43 -0
  14. package/docs/CLI_REFERENCE.md +97 -3
  15. package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
  16. package/docs/DOCUMENTATION_GUIDE.md +14 -0
  17. package/docs/GETTING_STARTED.md +1 -0
  18. package/docs/MCP.md +126 -0
  19. package/docs/RECIPES.md +82 -0
  20. package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
  21. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
  22. package/docs/TROUBLESHOOTING.md +111 -1
  23. package/docs/UNIVERSAL_INTEGRATION.md +48 -0
  24. package/package.json +14 -3
  25. package/schemas/task-recovery.schema.json +61 -0
  26. package/src/cli.js +173 -347
  27. package/src/commands/audit.js +5 -0
  28. package/src/commands/inspect.js +6 -0
  29. package/src/commands/progress.js +6 -2
  30. package/src/commands/status.js +17 -0
  31. package/src/commands/task-create.js +39 -1
  32. package/src/commands/task-list.js +14 -1
  33. package/src/commands/task-lock-status.js +2 -2
  34. package/src/commands/task-recover.js +202 -0
  35. package/src/commands/task-repair-legacy-recovery.js +417 -0
  36. package/src/commands/task-resume.js +172 -0
  37. package/src/commands/task-scope.js +23 -4
  38. package/src/commands/task-show.js +18 -4
  39. package/src/commands/validate-protocol.js +19 -2
  40. package/src/core/artifact-registry.js +12 -0
  41. package/src/core/audit.js +20 -4
  42. package/src/core/bundles.js +15 -0
  43. package/src/core/cli-command-definitions.js +50 -4
  44. package/src/core/command-executors.js +387 -0
  45. package/src/core/command-input.js +107 -0
  46. package/src/core/command-runtime.js +106 -0
  47. package/src/core/completion-artifacts.js +2 -3
  48. package/src/core/completion-ownership.js +88 -0
  49. package/src/core/error-codes.js +118 -1
  50. package/src/core/events.js +130 -1
  51. package/src/core/filesystem.js +55 -6
  52. package/src/core/inspect.js +27 -0
  53. package/src/core/integration-invocation-policy.js +170 -0
  54. package/src/core/integration-limits.js +20 -0
  55. package/src/core/integration-resources.js +127 -0
  56. package/src/core/next-action-model.js +60 -0
  57. package/src/core/next-action.js +31 -0
  58. package/src/core/phase.js +2 -1
  59. package/src/core/project-root.js +21 -0
  60. package/src/core/protocol-info.js +13 -0
  61. package/src/core/reconcile-closure.js +32 -10
  62. package/src/core/recovery-history.js +116 -0
  63. package/src/core/schema-validation.js +1 -0
  64. package/src/core/task-claim-state.js +272 -0
  65. package/src/core/task-command.js +5 -1
  66. package/src/core/task-conflict-inspection.js +321 -0
  67. package/src/core/task-context.js +32 -29
  68. package/src/core/task-discovery.js +14 -1
  69. package/src/core/task-lock.js +216 -22
  70. package/src/core/task-paths.js +3 -2
  71. package/src/core/task-recovery-migration.js +192 -0
  72. package/src/core/task-recovery.js +205 -0
  73. package/src/core/task-scope.js +33 -1
  74. package/src/core/templates.js +1 -0
  75. package/src/core/transaction.js +28 -2
  76. package/src/integration.js +47 -0
@@ -14,6 +14,11 @@ export async function runAudit(options = {}) {
14
14
 
15
15
  export function formatAuditResult(result) {
16
16
  const lines = [`FORGELOOP AUDIT: ${result.status}`];
17
+ if (result.recovery) lines.push(`RECOVERY: ${result.recovery.status} (${result.recovery.recoveryId})`);
18
+ if (result.claims) {
19
+ lines.push(`CLAIM STATE: ${result.claims.state}`);
20
+ lines.push(`MUTATION ALLOWED: ${result.claims.mutationAllowed ? "yes" : "no"}`);
21
+ }
17
22
  for (const error of result.errors) {
18
23
  lines.push(`${error.code}: ${error.message}`);
19
24
  if (error.next) lines.push(`NEXT: ${error.next}`);
@@ -19,6 +19,12 @@ export function formatInspectResult(report) {
19
19
  `Authority source: ${report.authority.sourceType ?? "none configured"} / ${report.authority.trusted ? "TRUSTED" : report.authority.trustMode === "NONE" ? "UNATTESTED" : "UNTRUSTED"}`,
20
20
  `Authority trust: ${report.authority.trustMode}`,
21
21
  `State: ${report.state.status}`,
22
+ ...(report.recovery
23
+ ? [`Recovery: ${report.recovery.status} (${report.recovery.recoveryId})`]
24
+ : []),
25
+ ...(report.claims
26
+ ? [`Claim state: ${report.claims.state}`, `Mutation allowed: ${report.claims.mutationAllowed ? "yes" : "no"}`]
27
+ : []),
22
28
  `Adapters: ${report.adapters.detected.length} detected`,
23
29
  `Findings: ${report.findings.length}`,
24
30
  report.ok ? "healthy: ForgeLoop target is ready" : "unhealthy: ForgeLoop target needs attention",
@@ -1,12 +1,16 @@
1
1
  import { evaluateProgress, PROGRESS_STATUS } from "../core/progress.js";
2
2
  import { readEvents } from "../core/events.js";
3
3
  import { readWorkState } from "../core/work-state.js";
4
- import { resolveTaskContext } from "../core/task-context.js";
4
+ import { resolveTaskContext, TASK_SELECTION_MODES } from "../core/task-context.js";
5
5
 
6
6
  export { evaluateProgress };
7
7
 
8
8
  export async function runProgress({ target, packageRoot, taskId, task }) {
9
- const resolved = await resolveTaskContext(target, { packageRoot, explicitTaskId: taskId ?? task });
9
+ const resolved = await resolveTaskContext(target, {
10
+ packageRoot,
11
+ taskId: taskId ?? task,
12
+ selectionMode: TASK_SELECTION_MODES.READ,
13
+ });
10
14
  const activeTaskId = resolved.taskId;
11
15
 
12
16
  const state = await readWorkState(target, { packageRoot, taskId: activeTaskId });
@@ -2,6 +2,7 @@ import { readAndClassifyWorkState } from "../core/work-state.js";
2
2
  import { inspectSchemaHealth } from "../core/schema-validation.js";
3
3
  import { reconcileContinuity } from "../core/continuity-reconciliation.js";
4
4
  import { withResolvedTask } from "../core/task-command.js";
5
+ import { findTaskById } from "../core/task-discovery.js";
5
6
 
6
7
  export async function runStatus({ target, packageRoot, contractFile = null, taskId, task } = {}) {
7
8
  return withResolvedTask(target, { taskId: taskId ?? task, packageRoot }, async (ctx) => {
@@ -11,10 +12,19 @@ export async function runStatus({ target, packageRoot, contractFile = null, task
11
12
  inspectSchemaHealth(target),
12
13
  reconcileContinuity({ target, packageRoot, taskId: effectiveTaskId }),
13
14
  ]);
15
+ const taskInfo = effectiveTaskId ? await findTaskById(target, effectiveTaskId, packageRoot) : null;
14
16
  return {
15
17
  ...state,
16
18
  taskId: effectiveTaskId,
17
19
  taskKey: ctx?.taskKey ?? null,
20
+ recovery: taskInfo?.recovery ?? null,
21
+ claimState: taskInfo?.claimState ?? null,
22
+ historicalWriteClaims: taskInfo?.historicalWriteClaims ?? [],
23
+ effectiveWriteClaims: taskInfo?.effectiveWriteClaims ?? [],
24
+ mutationAllowed: taskInfo?.mutationAllowed ?? true,
25
+ ownershipValid: taskInfo?.ownershipValid ?? true,
26
+ ownershipErrors: taskInfo?.ownershipErrors ?? taskInfo?.errors ?? [],
27
+ reasonCodes: taskInfo?.reasonCodes ?? [],
18
28
  protocol,
19
29
  continuity,
20
30
  evidence: [...(state.evidence ?? []), ...(protocol.evidence ?? [])],
@@ -31,6 +41,13 @@ export function formatStatusResult(result) {
31
41
  `Pending: ${result.pending.join(", ") || "none"}`,
32
42
  ];
33
43
  if (result.reasons.length > 0) lines.push(`Reasons: ${result.reasons.join(", ")}`);
44
+ if (result.recovery) {
45
+ lines.push(`Recovery: ${result.recovery.status} (${result.recovery.recoveryId})`);
46
+ }
47
+ if (result.claimState) {
48
+ lines.push(`Claim state: ${result.claimState}`);
49
+ lines.push(`Mutation allowed: ${result.mutationAllowed ? "yes" : "no"}`);
50
+ }
34
51
  if (result.warnings?.length > 0) lines.push(`Warnings: ${result.warnings.join(", ")}`);
35
52
  if (result.contractComparison) lines.push(`Contract: ${result.contractComparison}`);
36
53
  if (result.artifactComparison) lines.push(`Artifacts: ${result.artifactComparison}`);
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { assertTaskId } from "../core/task-identity.js";
3
3
  import { createTaskDescriptor, writeTaskDescriptor } from "../core/task-descriptor.js";
4
4
  import { normalizeWriteClaims, assertNoScopeConflicts, assertScopeClean } from "../core/task-scope.js";
5
+ import { inspectTaskConflictState } from "../core/task-conflict-inspection.js";
5
6
  import { discoverTasks, findTaskById } from "../core/task-discovery.js";
6
7
  import { withProjectClaimsLock } from "../core/task-lock.js";
7
8
  import { withTaskTransaction } from "../core/transaction.js";
@@ -10,6 +11,7 @@ import { ensureWithin, fileExists } from "../core/filesystem.js";
10
11
  import { validateContract, writeContract } from "../core/contract.js";
11
12
  import { appendProtocolEvent } from "../core/events.js";
12
13
  import { E_TASK_REQUIRED, E_TASK_ALREADY_EXISTS, E_TASK_DESCRIPTOR_INVALID } from "../core/error-codes.js";
14
+ import { recoveryGuidanceForClassification } from "../core/next-action-model.js";
13
15
 
14
16
  function taskError(code, message, artifacts = []) {
15
17
  const error = new Error(message);
@@ -18,6 +20,42 @@ function taskError(code, message, artifacts = []) {
18
20
  return error;
19
21
  }
20
22
 
23
+ export async function assertNoScopeConflictsWithInspection(claims, existingTasks, currentTaskId, { target, packageRoot } = {}) {
24
+ try {
25
+ assertNoScopeConflicts(claims, existingTasks, currentTaskId);
26
+ } catch (error) {
27
+ if (error.code !== "E_TASK_SCOPE_CONFLICT") throw error;
28
+ const inspected = [];
29
+ for (const conflict of error.conflicts ?? []) {
30
+ let inspection = null;
31
+ try {
32
+ inspection = await inspectTaskConflictState(target, { taskId: conflict.taskId, packageRoot });
33
+ } catch (inspectionError) {
34
+ inspection = {
35
+ taskId: conflict.taskId,
36
+ classification: "INCONSISTENT",
37
+ reasonCodes: [inspectionError.code ?? "E_TASK_NOT_FOUND"],
38
+ recoverable: false,
39
+ };
40
+ }
41
+ const guidance = recoveryGuidanceForClassification(inspection.classification, conflict.taskId);
42
+ inspected.push({
43
+ ...conflict,
44
+ classification: inspection.classification,
45
+ reasonCodes: inspection.reasonCodes,
46
+ nextAction: guidance.nextAction,
47
+ commandSpecs: guidance.commandSpecs,
48
+ inspection,
49
+ });
50
+ }
51
+ error.conflicts = inspected;
52
+ error.message = `${error.message}; conflicting task classifications: ${inspected
53
+ .map((item) => `${item.taskId}=${item.inspection.classification}`)
54
+ .join(", ")}`;
55
+ throw error;
56
+ }
57
+ }
58
+
21
59
  export async function runTaskCreate({ target, packageRoot, taskId, claims = [], contractFile = null } = {}) {
22
60
  if (!taskId) {
23
61
  throw taskError(E_TASK_REQUIRED, "--task is required for task-create");
@@ -33,7 +71,7 @@ export async function runTaskCreate({ target, packageRoot, taskId, claims = [],
33
71
 
34
72
  return withProjectClaimsLock(target, async () => {
35
73
  const allTasks = await discoverTasks(target, packageRoot);
36
- assertNoScopeConflicts(normalizedClaims, allTasks, taskId);
74
+ await assertNoScopeConflictsWithInspection(normalizedClaims, allTasks, taskId, { target, packageRoot });
37
75
  if (normalizedClaims.length > 0) {
38
76
  await assertScopeClean(target, normalizedClaims);
39
77
  }
@@ -20,6 +20,14 @@ export async function runTaskList({ target, packageRoot } = {}) {
20
20
  healthy: true,
21
21
  phase: task.phase,
22
22
  writeClaims: task.writeClaims ?? [],
23
+ historicalWriteClaims: task.historicalWriteClaims ?? [],
24
+ effectiveWriteClaims: task.effectiveWriteClaims ?? [],
25
+ claimState: task.claimState,
26
+ recovery: task.recovery,
27
+ mutationAllowed: task.mutationAllowed,
28
+ ownershipValid: task.ownershipValid,
29
+ ownershipErrors: task.ownershipErrors ?? task.errors ?? [],
30
+ reasonCodes: task.reasonCodes ?? [],
23
31
  locked: task.locked,
24
32
  hasContinuity: task.hasContinuity,
25
33
  hasReceipt: task.hasReceipt,
@@ -40,8 +48,13 @@ export function formatTaskListResult(result) {
40
48
  lines.push(`- ${task.taskKey} [CORRUPT]: ${task.error?.message ?? "unhealthy task namespace"}`);
41
49
  } else {
42
50
  const lockStr = task.locked ? " [LOCKED]" : "";
51
+ const recoveryStr = task.claimState === "RELEASED_BY_RECOVERY"
52
+ ? " [RECOVERED]"
53
+ : task.claimState === "INCONSISTENT"
54
+ ? " [OWNERSHIP INCONSISTENT]"
55
+ : "";
43
56
  const claimsStr = task.writeClaims.length > 0 ? ` (claims: ${task.writeClaims.join(", ")})` : "";
44
- lines.push(`- ${task.taskId}: ${task.phase ?? "UNINITIALIZED"}${lockStr}${claimsStr}`);
57
+ lines.push(`- ${task.taskId}: ${task.phase ?? "UNINITIALIZED"}${lockStr}${recoveryStr}${claimsStr}`);
45
58
  }
46
59
  }
47
60
  return `${lines.join("\n")}\n`;
@@ -1,8 +1,8 @@
1
- import { resolveTaskContext } from "../core/task-context.js";
1
+ import { resolveTaskContext, TASK_SELECTION_MODES } from "../core/task-context.js";
2
2
  import { classifyLockStaleness, readLockInfo } from "../core/task-lock.js";
3
3
 
4
4
  export async function runTaskLockStatus({ target, packageRoot, taskId } = {}) {
5
- const context = await resolveTaskContext(target, { taskId, packageRoot, explicitRequired: true });
5
+ const context = await resolveTaskContext(target, { taskId, packageRoot, explicitRequired: true, selectionMode: TASK_SELECTION_MODES.READ });
6
6
  const lock = await readLockInfo(target, context.taskId);
7
7
  if (!lock) {
8
8
  return {
@@ -0,0 +1,202 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import { resolveTaskContext } from "../core/task-context.js";
4
+ import { inspectTaskConflictState } from "../core/task-conflict-inspection.js";
5
+ import { appendProtocolEvent } from "../core/events.js";
6
+ import { withTaskTransaction } from "../core/transaction.js";
7
+ import { currentRepositoryFingerprint } from "../core/repository.js";
8
+ import {
9
+ readLockInfo,
10
+ releaseStaleTaskLockIfUnchanged,
11
+ withProjectClaimsLock,
12
+ } from "../core/task-lock.js";
13
+ import {
14
+ E_TASK_RECOVERY_AUTHORIZATION_REQUIRED,
15
+ E_TASK_RECOVERY_INCONSISTENT,
16
+ E_TASK_RECOVERY_OFFICIAL_PATH_AVAILABLE,
17
+ E_TASK_RECOVERY_UNSAFE,
18
+ E_TASK_ALREADY_RECOVERED,
19
+ } from "../core/error-codes.js";
20
+ import { readTaskDescriptor } from "../core/task-descriptor.js";
21
+ import {
22
+ createTaskRecovery,
23
+ writeTaskRecovery,
24
+ } from "../core/task-recovery.js";
25
+ import { resolveTaskClaimState } from "../core/task-claim-state.js";
26
+
27
+ export const TASK_RECOVERY_ALLOWED_CLASSIFICATIONS = Object.freeze(new Set([
28
+ "STALE",
29
+ "ABANDONED",
30
+ ]));
31
+
32
+ function recoveryError(code, message) {
33
+ const error = new Error(message);
34
+ error.code = code;
35
+ return error;
36
+ }
37
+
38
+ function alreadyRecoveredError(taskId, recovery) {
39
+ const error = recoveryError(E_TASK_ALREADY_RECOVERED, `Task ${taskId} is already RECOVERED`);
40
+ error.recovery = recovery;
41
+ return error;
42
+ }
43
+
44
+ async function assertConsistentRecoverableOwnership(target, taskId, packageRoot) {
45
+ const projection = await resolveTaskClaimState(target, { taskId, packageRoot });
46
+ if (!projection.valid) {
47
+ const error = recoveryError(
48
+ E_TASK_RECOVERY_INCONSISTENT,
49
+ `Task ${taskId} claim ownership is inconsistent; repair recovery state before task-recover`,
50
+ );
51
+ error.reasonCodes = projection.reasonCodes;
52
+ error.recoveryErrors = projection.ownershipErrors;
53
+ throw error;
54
+ }
55
+ if (projection.claimState === "RELEASED_BY_RECOVERY") {
56
+ throw alreadyRecoveredError(taskId, projection.recovery);
57
+ }
58
+ return projection;
59
+ }
60
+
61
+ function assertRecoveryAllowed(taskId, inspection) {
62
+ if (inspection.classification === "RECOVERABLE") {
63
+ throw recoveryError(
64
+ E_TASK_RECOVERY_OFFICIAL_PATH_AVAILABLE,
65
+ `Task ${taskId} is RECOVERABLE through canonical reconciliation; claim-release recovery is refused`,
66
+ );
67
+ }
68
+ if (inspection.classification === "INCONSISTENT") {
69
+ throw recoveryError(
70
+ E_TASK_RECOVERY_INCONSISTENT,
71
+ `Task ${taskId} state is INCONSISTENT (${inspection.reasonCodes.join(", ")}); repair the underlying artifact first`,
72
+ );
73
+ }
74
+ if (!TASK_RECOVERY_ALLOWED_CLASSIFICATIONS.has(inspection.classification)) {
75
+ throw recoveryError(
76
+ E_TASK_RECOVERY_UNSAFE,
77
+ `Task ${taskId} is ${inspection.classification} (${inspection.reasonCodes.join(", ")}); only STALE or ABANDONED tasks may release claims`,
78
+ );
79
+ }
80
+ }
81
+
82
+ export async function runTaskRecover({
83
+ target,
84
+ packageRoot,
85
+ taskId,
86
+ acknowledgeRecovery = false,
87
+ operatorAuthorized = false,
88
+ } = {}) {
89
+ const context = await resolveTaskContext(target, { taskId, packageRoot, explicitRequired: true });
90
+ const effectiveTaskId = context.taskId;
91
+
92
+ if (!acknowledgeRecovery && !operatorAuthorized) {
93
+ throw recoveryError(
94
+ E_TASK_RECOVERY_AUTHORIZATION_REQUIRED,
95
+ "task-recover requires explicit caller acknowledgement: re-run with --acknowledge-recovery",
96
+ );
97
+ }
98
+
99
+ await assertConsistentRecoverableOwnership(target, effectiveTaskId, packageRoot);
100
+
101
+ return withProjectClaimsLock(target, "task-recover", async () => {
102
+ await assertConsistentRecoverableOwnership(target, effectiveTaskId, packageRoot);
103
+
104
+ const inspectionBeforeLock = await inspectTaskConflictState(target, {
105
+ taskId: effectiveTaskId,
106
+ packageRoot,
107
+ });
108
+ assertRecoveryAllowed(effectiveTaskId, inspectionBeforeLock);
109
+
110
+ if (inspectionBeforeLock.evidence.lockStatus === "STALE") {
111
+ const expectedLock = await readLockInfo(target, effectiveTaskId);
112
+ const released = await releaseStaleTaskLockIfUnchanged(target, effectiveTaskId, expectedLock);
113
+ if (!released.released && released.reason !== "LOCK_MISSING") {
114
+ throw recoveryError(
115
+ E_TASK_RECOVERY_UNSAFE,
116
+ `Task ${effectiveTaskId} lock changed during recovery (${released.reason}); retry after re-inspection`,
117
+ );
118
+ }
119
+ }
120
+
121
+ return withTaskTransaction({
122
+ target,
123
+ taskId: effectiveTaskId,
124
+ operation: "task-recover",
125
+ packageRoot,
126
+ recordCommitEvent: true,
127
+ }, async (transaction) => {
128
+ await assertConsistentRecoverableOwnership(target, effectiveTaskId, packageRoot);
129
+
130
+ const inspection = await inspectTaskConflictState(target, {
131
+ taskId: effectiveTaskId,
132
+ packageRoot,
133
+ ignoredLockId: transaction.lock.lockId,
134
+ });
135
+ assertRecoveryAllowed(effectiveTaskId, inspection);
136
+ if (inspection.evidence.workStateRevision !== inspectionBeforeLock.evidence.workStateRevision
137
+ || inspection.evidence.ledgerLastSeq !== inspectionBeforeLock.evidence.ledgerLastSeq
138
+ || inspection.evidence.phase !== inspectionBeforeLock.evidence.phase) {
139
+ throw recoveryError(
140
+ E_TASK_RECOVERY_UNSAFE,
141
+ `Task ${effectiveTaskId} changed during recovery precondition validation`,
142
+ );
143
+ }
144
+
145
+ const descriptorArtifact = await readTaskDescriptor(target, context.taskKey, packageRoot);
146
+ const releasedClaims = descriptorArtifact?.value?.writeClaims ?? [];
147
+ const repository = await currentRepositoryFingerprint(target);
148
+ const recoveryId = `recovery-${randomUUID()}`;
149
+ const recoveredAt = new Date().toISOString();
150
+ const recoveryEvent = await appendProtocolEvent(target, {
151
+ taskId: effectiveTaskId,
152
+ event: "OPERATOR_RECOVERY_RECORDED",
153
+ at: recoveredAt,
154
+ details: {
155
+ recoveryId,
156
+ classification: inspection.classification,
157
+ reasonCodes: inspection.reasonCodes,
158
+ previousPhase: inspection.evidence.phase,
159
+ previousRevision: inspection.evidence.workStateRevision ?? 0,
160
+ previousHead: inspection.evidence.repositoryHead,
161
+ previousBranch: inspection.evidence.repositoryBranch,
162
+ currentHead: repository.head,
163
+ currentBranch: repository.branch,
164
+ releasedClaims,
165
+ authorityKind: "CALLER_ACKNOWLEDGED",
166
+ },
167
+ }, packageRoot, { taskId: effectiveTaskId });
168
+
169
+ await writeTaskRecovery(target, createTaskRecovery({
170
+ taskId: effectiveTaskId,
171
+ recoveredAt,
172
+ recoveryId,
173
+ recoveryEventSeq: recoveryEvent.seq,
174
+ classificationAtRecovery: inspection.classification,
175
+ reasonCodes: inspection.reasonCodes,
176
+ releasedClaims,
177
+ previousPhase: inspection.evidence.phase,
178
+ previousRevision: inspection.evidence.workStateRevision ?? 0,
179
+ repositoryFingerprint: repository,
180
+ authority: { kind: "CALLER_ACKNOWLEDGED" },
181
+ }), packageRoot);
182
+
183
+ return {
184
+ taskId: effectiveTaskId,
185
+ taskKey: context.taskKey,
186
+ recovered: true,
187
+ recoveryId,
188
+ classification: inspection.classification,
189
+ reasonCodes: inspection.reasonCodes,
190
+ phase: inspection.evidence.phase,
191
+ claimsReleased: true,
192
+ releasedClaims,
193
+ authority: { kind: "CALLER_ACKNOWLEDGED" },
194
+ message: `Task ${effectiveTaskId} recovered by caller acknowledgement; write claims released without completion claim`,
195
+ };
196
+ });
197
+ });
198
+ }
199
+
200
+ export function formatTaskRecoverResult(result) {
201
+ return `${result.message}\nclassification: ${result.classification}\nphase: ${result.phase} (unchanged)\n`;
202
+ }