@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.
- package/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +7 -0
- package/EXECUTION_STATE.md +40 -0
- package/LOOP_ENGINEERING.md +54 -5
- package/LOOP_SYSTEM_DESIGN.md +22 -1
- package/PROTOCOL_INTEGRATION.md +41 -0
- package/README.md +38 -0
- package/TERMINOLOGY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/THREAT_MODEL.md +20 -1
- package/docs/ARTIFACT_REFERENCE.md +43 -0
- package/docs/CLI_REFERENCE.md +97 -3
- package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
- package/docs/DOCUMENTATION_GUIDE.md +14 -0
- package/docs/GETTING_STARTED.md +1 -0
- package/docs/MCP.md +126 -0
- package/docs/RECIPES.md +82 -0
- package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
- package/docs/TROUBLESHOOTING.md +111 -1
- package/docs/UNIVERSAL_INTEGRATION.md +48 -0
- package/package.json +14 -3
- package/schemas/task-recovery.schema.json +61 -0
- package/src/cli.js +173 -347
- package/src/commands/audit.js +5 -0
- package/src/commands/inspect.js +6 -0
- package/src/commands/progress.js +6 -2
- package/src/commands/status.js +17 -0
- package/src/commands/task-create.js +39 -1
- package/src/commands/task-list.js +14 -1
- package/src/commands/task-lock-status.js +2 -2
- package/src/commands/task-recover.js +202 -0
- package/src/commands/task-repair-legacy-recovery.js +417 -0
- package/src/commands/task-resume.js +172 -0
- package/src/commands/task-scope.js +23 -4
- package/src/commands/task-show.js +18 -4
- package/src/commands/validate-protocol.js +19 -2
- package/src/core/artifact-registry.js +12 -0
- package/src/core/audit.js +20 -4
- package/src/core/bundles.js +15 -0
- package/src/core/cli-command-definitions.js +50 -4
- package/src/core/command-executors.js +387 -0
- package/src/core/command-input.js +107 -0
- package/src/core/command-runtime.js +106 -0
- package/src/core/completion-artifacts.js +2 -3
- package/src/core/completion-ownership.js +88 -0
- package/src/core/error-codes.js +118 -1
- package/src/core/events.js +130 -1
- package/src/core/filesystem.js +55 -6
- package/src/core/inspect.js +27 -0
- package/src/core/integration-invocation-policy.js +170 -0
- package/src/core/integration-limits.js +20 -0
- package/src/core/integration-resources.js +127 -0
- package/src/core/next-action-model.js +60 -0
- package/src/core/next-action.js +31 -0
- package/src/core/phase.js +2 -1
- package/src/core/project-root.js +21 -0
- package/src/core/protocol-info.js +13 -0
- package/src/core/reconcile-closure.js +32 -10
- package/src/core/recovery-history.js +116 -0
- package/src/core/schema-validation.js +1 -0
- package/src/core/task-claim-state.js +272 -0
- package/src/core/task-command.js +5 -1
- package/src/core/task-conflict-inspection.js +321 -0
- package/src/core/task-context.js +32 -29
- package/src/core/task-discovery.js +14 -1
- package/src/core/task-lock.js +216 -22
- package/src/core/task-paths.js +3 -2
- package/src/core/task-recovery-migration.js +192 -0
- package/src/core/task-recovery.js +205 -0
- package/src/core/task-scope.js +33 -1
- package/src/core/templates.js +1 -0
- package/src/core/transaction.js +28 -2
- 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",
|
package/src/core/next-action.js
CHANGED
|
@@ -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
|
|
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
|
|
25
|
-
* already satisfied in the current repository but whose
|
|
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
|
|
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
|
|
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
|
-
(
|
|
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",
|