@cassiomc1/forgeloop 1.2.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/AGENTS.md +1 -0
  3. package/AGENT_COMPATIBILITY.md +4 -0
  4. package/CLAUDE.md +1 -0
  5. package/DOCS_INDEX.md +14 -0
  6. package/EXECUTION_STATE.md +48 -0
  7. package/LOOP_ENGINEERING.md +55 -6
  8. package/LOOP_SYSTEM_DESIGN.md +32 -1
  9. package/PROTOCOL_INTEGRATION.md +71 -0
  10. package/README.md +86 -0
  11. package/TERMINOLOGY.md +15 -0
  12. package/THIRD_PARTY_NOTICES.md +15 -0
  13. package/THREAT_MODEL.md +22 -1
  14. package/docs/ARTIFACT_REFERENCE.md +54 -0
  15. package/docs/CLI_REFERENCE.md +177 -5
  16. package/docs/CROSS_HARNESS_CONTINUITY.md +34 -0
  17. package/docs/DOCUMENTATION_GUIDE.md +31 -0
  18. package/docs/GETTING_STARTED.md +10 -0
  19. package/docs/MCP.md +126 -0
  20. package/docs/RECIPES.md +87 -1
  21. package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
  22. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
  23. package/docs/TROUBLESHOOTING.md +243 -40
  24. package/docs/UNIVERSAL_INTEGRATION.md +48 -0
  25. package/package.json +17 -3
  26. package/schemas/execution.schema.json +11 -1
  27. package/schemas/task-recovery.schema.json +61 -0
  28. package/schemas/work-state.schema.json +1 -0
  29. package/src/cli.js +182 -337
  30. package/src/commands/audit.js +5 -0
  31. package/src/commands/doctor.js +22 -0
  32. package/src/commands/inspect.js +6 -0
  33. package/src/commands/migrate-protocol.js +18 -0
  34. package/src/commands/progress.js +6 -2
  35. package/src/commands/protocol-info.js +16 -0
  36. package/src/commands/run-check.js +2 -0
  37. package/src/commands/status.js +17 -0
  38. package/src/commands/task-create.js +42 -3
  39. package/src/commands/task-list.js +14 -1
  40. package/src/commands/task-lock-status.js +29 -0
  41. package/src/commands/task-recover.js +202 -0
  42. package/src/commands/task-repair-legacy-recovery.js +417 -0
  43. package/src/commands/task-resume.js +172 -0
  44. package/src/commands/task-scope.js +23 -4
  45. package/src/commands/task-show.js +21 -7
  46. package/src/commands/task-unlock.js +8 -6
  47. package/src/commands/validate-protocol.js +19 -2
  48. package/src/core/artifact-registry.js +12 -0
  49. package/src/core/artifacts.js +17 -4
  50. package/src/core/audit.js +20 -4
  51. package/src/core/bundles.js +15 -0
  52. package/src/core/cli-command-definitions.js +94 -4
  53. package/src/core/command-executors.js +387 -0
  54. package/src/core/command-input.js +107 -0
  55. package/src/core/command-runtime.js +106 -0
  56. package/src/core/completion-artifacts.js +17 -6
  57. package/src/core/completion-ownership.js +88 -0
  58. package/src/core/completion.js +15 -3
  59. package/src/core/diagnosis.js +15 -11
  60. package/src/core/error-codes.js +136 -1
  61. package/src/core/events.js +239 -9
  62. package/src/core/execution.js +73 -9
  63. package/src/core/filesystem.js +75 -8
  64. package/src/core/inspect.js +27 -0
  65. package/src/core/integration-invocation-policy.js +170 -0
  66. package/src/core/integration-limits.js +20 -0
  67. package/src/core/integration-resources.js +127 -0
  68. package/src/core/next-action-model.js +60 -0
  69. package/src/core/next-action.js +31 -0
  70. package/src/core/phase.js +10 -3
  71. package/src/core/project-root.js +21 -0
  72. package/src/core/protocol-info.js +54 -0
  73. package/src/core/protocol-migration.js +59 -0
  74. package/src/core/reconcile-closure.js +54 -14
  75. package/src/core/recovery-history.js +116 -0
  76. package/src/core/resumability.js +8 -6
  77. package/src/core/schema-validation.js +1 -0
  78. package/src/core/task-claim-state.js +272 -0
  79. package/src/core/task-command.js +8 -4
  80. package/src/core/task-conflict-inspection.js +321 -0
  81. package/src/core/task-context.js +32 -29
  82. package/src/core/task-discovery.js +14 -1
  83. package/src/core/task-lock.js +248 -18
  84. package/src/core/task-migration.js +24 -1
  85. package/src/core/task-paths.js +6 -3
  86. package/src/core/task-recovery-migration.js +192 -0
  87. package/src/core/task-recovery.js +205 -0
  88. package/src/core/task-scope.js +33 -1
  89. package/src/core/templates.js +1 -0
  90. package/src/core/transaction.js +285 -0
  91. package/src/core/work-state.js +70 -6
  92. 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
@@ -9,7 +9,7 @@ import {
9
9
  } from "./events.js";
10
10
  import { readPersistedRoute } from "./route-artifact.js";
11
11
  import { assertWorkPhase, isValidTransition } from "./protocol.js";
12
- import { readWorkState, writeWorkState } from "./work-state.js";
12
+ import { readWorkState, mutateWorkState } from "./work-state.js";
13
13
  import { evaluateCompletion } from "./completion.js";
14
14
  import { evaluatePreflight } from "./preflight.js";
15
15
  import { requiredEvidenceForTarget } from "./completion-artifacts.js";
@@ -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,
@@ -304,6 +305,7 @@ export async function advanceWorkState(target, toPhase, options = {}) {
304
305
  next.verificationCycle = reenteringVerification ? (state.verificationCycle ?? 1) + 1 : (state.verificationCycle ?? 1);
305
306
  }
306
307
  if (reenteringVerification) delete next.lastCompletionAttempt;
308
+ next.revision = (state.revision ?? 0) + 1;
307
309
  let nextReceipt = null;
308
310
  try {
309
311
  const receipt = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
@@ -361,7 +363,12 @@ export async function advanceWorkState(target, toPhase, options = {}) {
361
363
  } catch (error) {
362
364
  if (error.code !== "ARTIFACT_MISSING") throw error;
363
365
  }
364
- await writeWorkState(target, next, { packageRoot, taskId, statePath });
366
+ await mutateWorkState(target, {
367
+ expectedRevision: state.revision ?? 0,
368
+ packageRoot,
369
+ taskId,
370
+ statePath,
371
+ }, () => next);
365
372
  if (nextReceipt) {
366
373
  await writeJsonArtifact(target, receiptRel, nextReceipt, "execution-receipt", packageRoot);
367
374
  }
@@ -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
+ }
@@ -0,0 +1,54 @@
1
+ import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
2
+ import { ARTIFACT_REGISTRY } from "./artifact-registry.js";
3
+ import { PUBLIC_ERROR_REGISTRY } from "./error-codes.js";
4
+ import { GUIDE_REGISTRY } from "./guide-registry.js";
5
+ import { PROTOCOL_VERSION, WORK_PHASES, WORK_TRANSITIONS } from "./protocol.js";
6
+
7
+ export const SCHEMA_COMPATIBILITY_POLICY = Object.freeze({
8
+ protocolVersion: PROTOCOL_VERSION,
9
+ schemaVersion: 1,
10
+ read: "Readers reject unknown protocol or schema versions; compatibility changes require a new published version.",
11
+ write: "Writers emit only the current schema and protocol versions.",
12
+ migration: "Use migrate-protocol --to <version> --dry-run to plan an explicit supported migration. Legacy singleton artifacts are migrated with a receipt-backed task-migrate action.",
13
+ });
14
+
15
+ function publicSchemaVersions() {
16
+ return Object.fromEntries(
17
+ [...new Set(Object.values(ARTIFACT_REGISTRY)
18
+ .filter((artifact) => artifact.isPublic && artifact.isPersisted)
19
+ .map((artifact) => artifact.schema))]
20
+ .sort()
21
+ .map((schema) => [schema, [SCHEMA_COMPATIBILITY_POLICY.schemaVersion]]),
22
+ );
23
+ }
24
+
25
+ export function protocolInfo({ packageVersion = null } = {}) {
26
+ const errors = Object.values(PUBLIC_ERROR_REGISTRY);
27
+ const schemaVersions = publicSchemaVersions();
28
+ return {
29
+ packageVersion,
30
+ protocolVersion: PROTOCOL_VERSION,
31
+ readsProtocol: [PROTOCOL_VERSION],
32
+ writesProtocol: [PROTOCOL_VERSION],
33
+ readsSchemaVersions: schemaVersions,
34
+ writesSchemaVersions: schemaVersions,
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
+ },
49
+ lifecycle: { phases: WORK_PHASES, transitions: WORK_TRANSITIONS },
50
+ guides: Object.values(GUIDE_REGISTRY),
51
+ commands: Object.values(CLI_COMMAND_DEFINITIONS).map(({ name, category, mutation, description }) => ({ name, category, mutation, description })),
52
+ errors,
53
+ };
54
+ }
@@ -0,0 +1,59 @@
1
+ import { E_PROTOCOL_MIGRATION_TARGET_UNSUPPORTED } from "./error-codes.js";
2
+ import { PROTOCOL_VERSION } from "./protocol.js";
3
+ import { detectLegacySingletonLayout, migrateLegacyLayout } from "./task-migration.js";
4
+ import { getPackageRoot } from "./templates.js";
5
+
6
+ function assertSupportedTarget(to) {
7
+ if (to === undefined || to === null || to === "") {
8
+ const error = new Error("migrate-protocol requires --to <protocolVersion>");
9
+ error.code = E_PROTOCOL_MIGRATION_TARGET_UNSUPPORTED;
10
+ throw error;
11
+ }
12
+
13
+ if (String(to) !== String(PROTOCOL_VERSION)) {
14
+ const error = new Error(
15
+ `Protocol version ${to} is not supported by this ForgeLoop release; supported target: ${PROTOCOL_VERSION}.`,
16
+ );
17
+ error.code = E_PROTOCOL_MIGRATION_TARGET_UNSUPPORTED;
18
+ throw error;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Migrates only state that has an explicitly supported, receipt-backed path.
24
+ * Future protocol versions must add a dedicated migration before becoming an
25
+ * accepted target here; accepting an unknown target would risk silent rewrite.
26
+ */
27
+ export async function migrateProtocol(
28
+ target,
29
+ { to, dryRun = false, packageRoot = getPackageRoot() } = {},
30
+ ) {
31
+ assertSupportedTarget(to);
32
+ const legacy = await detectLegacySingletonLayout(target);
33
+
34
+ if (!legacy.hasLegacy) {
35
+ return {
36
+ migrated: false,
37
+ dryRun,
38
+ fromProtocol: PROTOCOL_VERSION,
39
+ toProtocol: PROTOCOL_VERSION,
40
+ status: "ALREADY_COMPATIBLE",
41
+ actions: [],
42
+ message: `Target already uses supported protocol ${PROTOCOL_VERSION}; no migration is required.`,
43
+ };
44
+ }
45
+
46
+ const result = await migrateLegacyLayout(target, { dryRun, packageRoot });
47
+ return {
48
+ ...result,
49
+ fromProtocol: PROTOCOL_VERSION,
50
+ toProtocol: PROTOCOL_VERSION,
51
+ status: dryRun ? "PLANNED_LEGACY_LAYOUT_MIGRATION" : "MIGRATED_LEGACY_LAYOUT",
52
+ actions: [{
53
+ kind: "LEGACY_LAYOUT_MIGRATION",
54
+ command: "task-migrate",
55
+ receipt: ".forgeloop/task-state/<taskKey>/migration-receipt.json",
56
+ artifacts: legacy.legacyFiles.map((item) => item.path),
57
+ }],
58
+ };
59
+ }