@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
@@ -0,0 +1,170 @@
1
+ import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
2
+ import { COMMAND_EXECUTORS } from "./command-executors.js";
3
+ import { PROTOCOL_VERSION } from "./protocol.js";
4
+ import { FORGELOOP_INTEGRATION_RUNTIME_VERSION } from "./command-runtime.js";
5
+
6
+ /**
7
+ * Integration risk classes. They classify the *invocation*, not only the
8
+ * command name: input-dependent commands (doctor --fix, task-unlock --force,
9
+ * policy-discover --write, baseline mutations) are refined by the sparse
10
+ * override table below.
11
+ */
12
+ export const INTEGRATION_RISK_CLASSES = Object.freeze({
13
+ READ_ONLY: "READ_ONLY",
14
+ LOOP_MUTATION: "LOOP_MUTATION",
15
+ CLAIM_REACQUISITION: "CLAIM_REACQUISITION",
16
+ EXTERNAL_EXECUTION: "EXTERNAL_EXECUTION",
17
+ MAINTENANCE: "MAINTENANCE",
18
+ CLAIM_RELEASE_RECOVERY: "CLAIM_RELEASE_RECOVERY",
19
+ LEGACY_MIGRATION: "LEGACY_MIGRATION",
20
+ FORCE_DESTRUCTIVE: "FORCE_DESTRUCTIVE",
21
+ });
22
+
23
+ const READ_ONLY_COMMANDS = Object.freeze(new Set([
24
+ "protocol-info", "status", "next", "continuity", "reconcile-continuity",
25
+ "task-list", "task-show", "task-lock-status", "progress", "audit", "report",
26
+ "inspect", "validate-state", "validate-protocol", "validate-receipt",
27
+ "policy-status", "policy-diff", "rule-verify", "policy",
28
+ ]));
29
+
30
+ const LOOP_MUTATION_COMMANDS = Object.freeze(new Set([
31
+ "route", "preflight", "advance", "task-create", "task-scope",
32
+ "record-continuity", "clear-continuity", "prepare-completion",
33
+ "record-check", "record-diagnosis", "record-decision-criterion",
34
+ "record-terminal-result", "complete",
35
+ ]));
36
+
37
+ const STATIC_RISK_CLASSES = Object.freeze({
38
+ ...Object.fromEntries([...READ_ONLY_COMMANDS].map((name) => [name, INTEGRATION_RISK_CLASSES.READ_ONLY])),
39
+ ...Object.fromEntries([...LOOP_MUTATION_COMMANDS].map((name) => [name, INTEGRATION_RISK_CLASSES.LOOP_MUTATION])),
40
+ "task-resume": INTEGRATION_RISK_CLASSES.CLAIM_REACQUISITION,
41
+ "run-check": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
42
+ "reconcile-closure": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
43
+ init: INTEGRATION_RISK_CLASSES.MAINTENANCE,
44
+ update: INTEGRATION_RISK_CLASSES.MAINTENANCE,
45
+ activate: INTEGRATION_RISK_CLASSES.MAINTENANCE,
46
+ "task-migrate": INTEGRATION_RISK_CLASSES.MAINTENANCE,
47
+ "migrate-protocol": INTEGRATION_RISK_CLASSES.MAINTENANCE,
48
+ "clear-state": INTEGRATION_RISK_CLASSES.MAINTENANCE,
49
+ doctor: INTEGRATION_RISK_CLASSES.MAINTENANCE,
50
+ "policy-discover": INTEGRATION_RISK_CLASSES.MAINTENANCE,
51
+ baseline: INTEGRATION_RISK_CLASSES.MAINTENANCE,
52
+ "task-unlock": INTEGRATION_RISK_CLASSES.MAINTENANCE,
53
+ // bundle writes a bundle artifact set under the task namespace; it is not
54
+ // read-only despite producing no protocol-state mutations.
55
+ bundle: INTEGRATION_RISK_CLASSES.MAINTENANCE,
56
+ "profile-interview": INTEGRATION_RISK_CLASSES.MAINTENANCE,
57
+ "task-recover": INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY,
58
+ "task-repair-legacy-recovery": INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION,
59
+ });
60
+
61
+ // Sparse input-dependent refinements over the static table.
62
+ function refineRiskClass(command, input) {
63
+ if (command === "task-unlock" && input?.force === true) {
64
+ return INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE;
65
+ }
66
+ // Fail closed: every canonical command must be explicitly classified.
67
+ return baseRiskClass(command);
68
+ }
69
+
70
+ export function baseRiskClass(command) {
71
+ const riskClass = STATIC_RISK_CLASSES[command];
72
+ if (!riskClass) {
73
+ throw new Error(`Command ${command} has no integration risk classification`);
74
+ }
75
+ return riskClass;
76
+ }
77
+
78
+ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
79
+ const commands = Object.keys(CLI_COMMAND_DEFINITIONS).sort().map((name) => {
80
+ const def = CLI_COMMAND_DEFINITIONS[name];
81
+ return {
82
+ name,
83
+ category: def.category,
84
+ mutation: def.mutation,
85
+ baseRiskClass: baseRiskClass(name),
86
+ mayExecuteExternalProcess: def.mayExecuteExternalProcess === true,
87
+ description: def.description,
88
+ };
89
+ });
90
+ return {
91
+ packageVersion,
92
+ protocolVersion: PROTOCOL_VERSION,
93
+ integrationApiVersion: FORGELOOP_INTEGRATION_RUNTIME_VERSION,
94
+ executorParity: Object.keys(COMMAND_EXECUTORS).length === Object.keys(CLI_COMMAND_DEFINITIONS).length,
95
+ features: {
96
+ taskClaimRecovery: {
97
+ version: 1,
98
+ durableRecoveryState: true,
99
+ explicitResume: true,
100
+ validatedClaimProjection: true,
101
+ },
102
+ },
103
+ commands,
104
+ resources: [
105
+ { name: "protocol/info", scope: "PROJECT" },
106
+ { name: "project/tasks", scope: "PROJECT" },
107
+ { name: "task/status", scope: "TASK" },
108
+ { name: "task/ownership", scope: "TASK" },
109
+ { name: "task/contract", scope: "TASK" },
110
+ { name: "task/continuity", scope: "TASK" },
111
+ ],
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Classify a concrete command invocation (command + structured input).
117
+ * Tool-provided input can never elevate a launch-level capability; this
118
+ * classifier only describes what the invocation would do.
119
+ */
120
+ export function classifyForgeLoopInvocation(command, input = {}) {
121
+ const definition = CLI_COMMAND_DEFINITIONS[command];
122
+ if (!definition) {
123
+ throw new Error(`Unknown ForgeLoop command: ${command}`);
124
+ }
125
+ const riskClass = refineRiskClass(command, input);
126
+ const readOnly = riskClass === INTEGRATION_RISK_CLASSES.READ_ONLY;
127
+ const requiredCapability = (() => {
128
+ switch (riskClass) {
129
+ case INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION:
130
+ return "allowExternalExecution";
131
+ case INTEGRATION_RISK_CLASSES.MAINTENANCE:
132
+ return "allowMaintenance";
133
+ case INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY:
134
+ return "allowRecovery";
135
+ case INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION:
136
+ return "allowLegacyRepair";
137
+ case INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE:
138
+ return "allowForceRecovery";
139
+ default:
140
+ return null;
141
+ }
142
+ })();
143
+ return Object.freeze({
144
+ command,
145
+ riskClass,
146
+ readOnly,
147
+ mutatesProtocol: !readOnly && [
148
+ INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
149
+ INTEGRATION_RISK_CLASSES.CLAIM_REACQUISITION,
150
+ INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
151
+ INTEGRATION_RISK_CLASSES.MAINTENANCE,
152
+ INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY,
153
+ INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION,
154
+ INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE,
155
+ ].includes(riskClass),
156
+ removesArtifacts: definition.removes.length > 0,
157
+ executesExternalProcess: definition.mayExecuteExternalProcess === true,
158
+ affectsClaimAuthority: [
159
+ "task-resume", "task-recover", "task-repair-legacy-recovery",
160
+ "task-create", "task-scope", "complete",
161
+ ].includes(command),
162
+ destructive: [
163
+ INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE,
164
+ INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY,
165
+ INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION,
166
+ INTEGRATION_RISK_CLASSES.MAINTENANCE,
167
+ ].includes(riskClass),
168
+ requiredCapability,
169
+ });
170
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Bounded input limits for structured integrations. These bound the
3
+ * transport-facing surface; canonical ForgeLoop JSON safety limits remain
4
+ * authoritative for persisted artifacts. Values are conservative and
5
+ * intentionally small for an agent-facing API.
6
+ */
7
+ export const INTEGRATION_LIMITS = Object.freeze({
8
+ /** Maximum length of any single string input (task IDs, names, text). */
9
+ maxStringLength: 4096,
10
+ /** Maximum number of entries in a repeatable string option. */
11
+ maxRepeatedValues: 32,
12
+ /** Maximum number of exact argv items passed to external execution. */
13
+ maxArgvItems: 64,
14
+ /** Maximum length of a single argv item. */
15
+ maxArgvItemLength: 2048,
16
+ /** Maximum serialized size of a JSON-object input field. */
17
+ maxStructuredInputBytes: 256 * 1024,
18
+ /** Maximum serialized size of any tool/resource response payload. */
19
+ maxOutputBytes: 4 * 1024 * 1024,
20
+ });
@@ -0,0 +1,127 @@
1
+ import { runProtocolInfo } from "../commands/protocol-info.js";
2
+ import { readContract } from "./contract.js";
3
+ import { discoverTasks } from "./task-discovery.js";
4
+ import { resolveTaskClaimState } from "./task-claim-state.js";
5
+ import { runStatus } from "../commands/status.js";
6
+ import { runContinuity } from "../commands/continuity.js";
7
+
8
+ /**
9
+ * Canonical integration resource allowlist.
10
+ *
11
+ * This is NOT a replacement for ARTIFACT_REGISTRY: the artifact registry
12
+ * describes persisted protocol artifacts; this registry describes what is
13
+ * safe to expose through structured integrations. Anything not listed here
14
+ * must never be readable through an integration transport.
15
+ *
16
+ * task/ownership is derived exclusively from resolveTaskClaimState() — the
17
+ * canonical claim resolver. Integrations must present these values; they
18
+ * must never derive them from raw artifacts such as task.json or
19
+ * recovery.json.
20
+ */
21
+ export const INTEGRATION_RESOURCE_DEFINITIONS = Object.freeze({
22
+ "protocol/info": Object.freeze({
23
+ scope: "PROJECT",
24
+ description: "ForgeLoop protocol version, schema compatibility, features, and command metadata.",
25
+ }),
26
+ "project/tasks": Object.freeze({
27
+ scope: "PROJECT",
28
+ description: "All discovered tasks with canonical ownership projection fields.",
29
+ }),
30
+ "task/status": Object.freeze({
31
+ scope: "TASK",
32
+ description: "Canonical status projection for one task, including ownership fields.",
33
+ }),
34
+ "task/ownership": Object.freeze({
35
+ scope: "TASK",
36
+ description: "Canonical validated claim ownership for one task.",
37
+ }),
38
+ "task/contract": Object.freeze({
39
+ scope: "TASK",
40
+ description: "The task's current contract.",
41
+ }),
42
+ "task/continuity": Object.freeze({
43
+ scope: "TASK",
44
+ description: "Cross-harness continuity state for one task.",
45
+ }),
46
+ });
47
+
48
+ function ownershipProjection(projection) {
49
+ return {
50
+ taskId: projection.taskId,
51
+ phase: projection.phase,
52
+ claimState: projection.claimState,
53
+ mutationAllowed: projection.mutationAllowed,
54
+ ownershipValid: projection.ownershipValid,
55
+ recoveryStatus: projection.recoveryStatus,
56
+ historicalWriteClaims: [...projection.historicalWriteClaims],
57
+ effectiveWriteClaims: [...projection.effectiveWriteClaims],
58
+ reasonCodes: [...projection.reasonCodes],
59
+ };
60
+ }
61
+
62
+ export async function readForgeLoopIntegrationResource(uri, {
63
+ projectPath = ".",
64
+ packageRoot = undefined,
65
+ packageVersion = null,
66
+ taskId = null,
67
+ } = {}) {
68
+ const resource = INTEGRATION_RESOURCE_DEFINITIONS[uri];
69
+ if (!resource) {
70
+ const error = new Error(`Unknown ForgeLoop integration resource: ${uri}`);
71
+ error.code = "E_INTEGRATION_RESOURCE_UNKNOWN";
72
+ throw error;
73
+ }
74
+
75
+ switch (uri) {
76
+ case "protocol/info": {
77
+ return { uri, data: await runProtocolInfo({ packageVersion }) };
78
+ }
79
+ case "project/tasks": {
80
+ const tasks = await discoverTasks(projectPath, packageRoot);
81
+ return {
82
+ uri,
83
+ data: {
84
+ count: tasks.length,
85
+ tasks: tasks.map((task) => ({
86
+ taskId: task.taskId,
87
+ healthy: task.healthy !== false,
88
+ phase: task.phase ?? null,
89
+ mutationAllowed: task.mutationAllowed !== false,
90
+ })),
91
+ },
92
+ };
93
+ }
94
+ case "task/status":
95
+ case "task/ownership":
96
+ case "task/contract":
97
+ case "task/continuity": {
98
+ if (typeof taskId !== "string" || !taskId) {
99
+ const error = new Error(`Resource ${uri} requires a taskId`);
100
+ error.code = "E_TASK_REQUIRED";
101
+ throw error;
102
+ }
103
+ break;
104
+ }
105
+ }
106
+
107
+ if (uri === "task/ownership") {
108
+ const projection = await resolveTaskClaimState(projectPath, { taskId, packageRoot });
109
+ return { uri, taskId, data: ownershipProjection(projection) };
110
+ }
111
+ if (uri === "task/status") {
112
+ const result = await runStatus({ target: projectPath, packageRoot, taskId });
113
+ return { uri, taskId, data: result };
114
+ }
115
+ if (uri === "task/contract") {
116
+ try {
117
+ const contract = await readContract(projectPath, packageRoot, { taskId });
118
+ return { uri, taskId, data: contract.value };
119
+ } catch (error) {
120
+ error.message = `Resource task/contract unavailable: ${error.message}`;
121
+ throw error;
122
+ }
123
+ }
124
+ // task/continuity
125
+ const continuity = await runContinuity({ target: projectPath, packageRoot, taskId });
126
+ return { uri, taskId, data: continuity };
127
+ }
@@ -31,9 +31,69 @@ export const NEXT_ACTIONS = Object.freeze({
31
31
  REPAIR_POLICY: "REPAIR_POLICY",
32
32
  RESTORE_BASELINE: "RESTORE_BASELINE",
33
33
  CONTINUE_WITH_EXISTING_BASELINE: "CONTINUE_WITH_EXISTING_BASELINE",
34
+ RECONCILE_CLOSURE: "RECONCILE_CLOSURE",
35
+ RECOVER_TASK: "RECOVER_TASK",
36
+ RESUME_RECOVERED_TASK: "RESUME_RECOVERED_TASK",
37
+ RESOLVE_RECOVERY_INCONSISTENCY: "RESOLVE_RECOVERY_INCONSISTENCY",
34
38
  NONE: "NONE",
35
39
  });
36
40
 
41
+ function directCommandSpec(commandId, taskId, requiredInputs = []) {
42
+ return {
43
+ commandId,
44
+ executable: "forgeloop",
45
+ subcommand: commandId,
46
+ argv: [commandId, `--task=${taskId}`, "--json"],
47
+ requiredInputs,
48
+ };
49
+ }
50
+
51
+ export function recoveryGuidanceForClassification(classification, taskId) {
52
+ if (classification === "RECOVERABLE") {
53
+ return {
54
+ nextAction: NEXT_ACTIONS.RECONCILE_CLOSURE,
55
+ commands: ["forgeloop reconcile-closure --task <id>"],
56
+ commandSpecs: [directCommandSpec("reconcile-closure", taskId, [
57
+ { name: "checkId", option: "--id=<contract-verification-id>" },
58
+ { name: "requirement", option: "--requirement=<exact-contract-verification-text>" },
59
+ { name: "command", option: "-- <verification-command...>" },
60
+ ])],
61
+ };
62
+ }
63
+ if (classification === "STALE" || classification === "ABANDONED") {
64
+ return {
65
+ nextAction: NEXT_ACTIONS.RECOVER_TASK,
66
+ commands: ["forgeloop task-recover --task <id> --acknowledge-recovery --json"],
67
+ commandSpecs: [directCommandSpec("task-recover", taskId, [
68
+ {
69
+ name: "acknowledgeRecovery",
70
+ option: "--acknowledge-recovery",
71
+ description: "Caller acknowledgement only; not host-attested authority.",
72
+ },
73
+ ])],
74
+ };
75
+ }
76
+ if (classification === "RECOVERED") {
77
+ return {
78
+ nextAction: NEXT_ACTIONS.RESUME_RECOVERED_TASK,
79
+ commands: ["forgeloop task-resume --task <id> --json"],
80
+ commandSpecs: [directCommandSpec("task-resume", taskId)],
81
+ };
82
+ }
83
+ if (classification === "INCONSISTENT") {
84
+ return {
85
+ nextAction: NEXT_ACTIONS.RESOLVE_RECOVERY_INCONSISTENCY,
86
+ commands: ["forgeloop validate-protocol --task <id> --json"],
87
+ commandSpecs: [directCommandSpec("validate-protocol", taskId)],
88
+ };
89
+ }
90
+ return {
91
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
92
+ commands: ["forgeloop task-show --task <id> --json"],
93
+ commandSpecs: [directCommandSpec("task-show", taskId)],
94
+ };
95
+ }
96
+
37
97
  export function result({
38
98
  taskId = "unknown",
39
99
  currentPhase = "RECEIVED",
@@ -20,6 +20,7 @@ import {
20
20
  recordCheckCommandSpec,
21
21
  recordDiagnosisCommandSpec,
22
22
  recordTerminalResultCommandSpec,
23
+ recoveryGuidanceForClassification,
23
24
  result,
24
25
  uniqueSorted,
25
26
  } from "./next-action-model.js";
@@ -37,6 +38,7 @@ import { currentCycleDiagnosis } from "./diagnosis-model.js";
37
38
  import { evaluateProgress, PROGRESS_STATUS } from "./progress.js";
38
39
  import { criterionForDecision } from "./settlement-model.js";
39
40
  import { readEvents } from "./events.js";
41
+ import { inspectTaskConflictState } from "./task-conflict-inspection.js";
40
42
 
41
43
  export { NEXT_ACTIONS } from "./next-action-model.js";
42
44
 
@@ -145,6 +147,35 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
145
147
 
146
148
  const state = workState.value;
147
149
  const context = { taskId: state.taskId, currentPhase: state.phase };
150
+ if (explicitTaskId) {
151
+ let inspection;
152
+ try {
153
+ inspection = await inspectTaskConflictState(target, {
154
+ taskId: explicitTaskId,
155
+ packageRoot,
156
+ });
157
+ } catch (error) {
158
+ inspection = {
159
+ classification: "INCONSISTENT",
160
+ reasonCodes: [error.code ?? "E_TASK_RECOVERY_INCONSISTENT"],
161
+ };
162
+ }
163
+ if (!["ACTIVE", "COMPLETE"].includes(inspection.classification)) {
164
+ const guidance = recoveryGuidanceForClassification(inspection.classification, explicitTaskId);
165
+ return result({
166
+ ...context,
167
+ nextAction: guidance.nextAction,
168
+ commands: guidance.commands,
169
+ commandSpecs: guidance.commandSpecs,
170
+ reasons: inspection.reasonCodes.map((code) => artifactError(
171
+ code,
172
+ `Task conflict state is ${inspection.classification}; follow the structured recovery guidance.`,
173
+ [stateRel, eventsRel, taskArtifactPath(explicitTaskId, "recovery")],
174
+ )),
175
+ requiredArtifacts: [stateRel, eventsRel],
176
+ });
177
+ }
178
+ }
148
179
  if (state.phase === "RECEIVED") {
149
180
  return decision(
150
181
  context,
package/src/core/phase.js CHANGED
@@ -187,7 +187,8 @@ export async function advanceWorkState(target, toPhase, options = {}) {
187
187
 
188
188
  const nonCompleteTasks = discovered.filter((t) => t.phase !== "COMPLETE");
189
189
  if (nonCompleteTasks.length > 1) {
190
- const claims = descriptor?.writeClaims ?? [];
190
+ const currentTask = discovered.find((task) => task.taskId === taskId && task.healthy !== false);
191
+ const claims = currentTask?.writeClaims ?? [];
191
192
  if (claims.length === 0) {
192
193
  throw phaseError(
193
194
  E_TASK_SCOPE_REQUIRED,
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ import { realpath } from "node:fs/promises";
3
+
4
+ import { resolveTarget } from "./filesystem.js";
5
+
6
+ /**
7
+ * Canonical project-root resolution for integrations. Applies exactly the
8
+ * same semantics as the CLI target resolver — the path must exist, must be a
9
+ * real directory (not a symlink), and is returned as an absolute path.
10
+ *
11
+ * Symlinked roots are rejected so that every transport agrees on whether a
12
+ * given project path is acceptable; use the resolved real directory instead.
13
+ */
14
+ export async function resolveForgeLoopProjectRoot(projectPath, { cwd = process.cwd() } = {}) {
15
+ const target = await resolveTarget(cwd, projectPath);
16
+ return realpath(target);
17
+ }
18
+
19
+ export function defaultIntegrationProjectPath() {
20
+ return path.resolve(".");
21
+ }
@@ -33,6 +33,19 @@ export function protocolInfo({ packageVersion = null } = {}) {
33
33
  readsSchemaVersions: schemaVersions,
34
34
  writesSchemaVersions: schemaVersions,
35
35
  compatibility: SCHEMA_COMPATIBILITY_POLICY,
36
+ features: {
37
+ taskClaimRecovery: {
38
+ version: 1,
39
+ durableRecoveryState: true,
40
+ explicitResume: true,
41
+ validatedClaimProjection: true,
42
+ },
43
+ integrationApi: {
44
+ version: 1,
45
+ structuredCommandRuntime: true,
46
+ canonicalResources: true,
47
+ },
48
+ },
36
49
  lifecycle: { phases: WORK_PHASES, transitions: WORK_TRANSITIONS },
37
50
  guides: Object.values(GUIDE_REGISTRY),
38
51
  commands: Object.values(CLI_COMMAND_DEFINITIONS).map(({ name, category, mutation, description }) => ({ name, category, mutation, description })),
@@ -1,6 +1,6 @@
1
1
  import { readContract } from "./contract.js";
2
2
  import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
- import { appendProtocolEvent, validateEventLedger } from "./events.js";
3
+ import { appendProtocolEvent, validateEventLedger, readEvents, validateCompletionRecoveryAuthorization } from "./events.js";
4
4
  import { runCommandExecution } from "./execution.js";
5
5
  import { createReceipt } from "./receipt.js";
6
6
  import { currentRepositoryFingerprint } from "./repository.js";
@@ -11,7 +11,7 @@ export const RECONCILE_EVENT = "CHECKPOINT_RECONCILED";
11
11
 
12
12
  const RECONCILABLE_DRIFT = new Set(["REPOSITORY_CHANGED"]);
13
13
 
14
- const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING"]);
14
+ const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING", "REVIEWING"]);
15
15
 
16
16
  function reconcileError(code, message, artifacts = []) {
17
17
  const error = new Error(message);
@@ -21,12 +21,13 @@ function reconcileError(code, message, artifacts = []) {
21
21
  }
22
22
 
23
23
  /**
24
- * Canonical recovery for an EXECUTING or VERIFYING task whose objective is
25
- * already satisfied in the current repository but whose work-state
26
- * 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.
27
27
  *
28
28
  * The command refreshes the checkpoint repository fingerprint only after:
29
- * - the task is EXECUTING or VERIFYING,
29
+ * - the task is EXECUTING, VERIFYING, or (with authorized completion
30
+ * recovery) REVIEWING,
30
31
  * - classification requires revalidation and the only drift is
31
32
  * REPOSITORY_CHANGED,
32
33
  * - the append-only event ledger is valid,
@@ -74,10 +75,28 @@ export async function runReconcileClosure({
74
75
  if (!RECONCILABLE_PHASES.has(state.phase)) {
75
76
  throw reconcileError(
76
77
  "E_RECONCILE_PHASE_INVALID",
77
- `reconcile-closure supports EXECUTING or VERIFYING tasks whose objective is already satisfied; found ${state.phase}`,
78
+ `reconcile-closure supports EXECUTING, VERIFYING, or REVIEWING tasks whose objective is already satisfied; found ${state.phase}`,
78
79
  [stateRel],
79
80
  );
80
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
+ }
81
100
 
82
101
  const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
83
102
  if (freshness.status !== "REVALIDATION_REQUIRED" || !freshness.reasons.includes("REPOSITORY_CHANGED")) {
@@ -107,9 +126,12 @@ export async function runReconcileClosure({
107
126
  }
108
127
 
109
128
  const contract = await readContract(target, packageRoot, { taskId });
110
- const verificationItem = (contract.value.verification ?? []).find(
111
- (item) => item.type === "VERIFICATION" && item.id === checkId && item.text === requirement,
112
- );
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
+ });
113
135
  if (!verificationItem) {
114
136
  throw reconcileError(
115
137
  "E_RECONCILE_REQUIREMENT_UNKNOWN",