@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.
- package/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +1 -0
- package/AGENT_COMPATIBILITY.md +4 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +14 -0
- package/EXECUTION_STATE.md +48 -0
- package/LOOP_ENGINEERING.md +55 -6
- package/LOOP_SYSTEM_DESIGN.md +32 -1
- package/PROTOCOL_INTEGRATION.md +71 -0
- package/README.md +86 -0
- package/TERMINOLOGY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/THREAT_MODEL.md +22 -1
- package/docs/ARTIFACT_REFERENCE.md +54 -0
- package/docs/CLI_REFERENCE.md +177 -5
- package/docs/CROSS_HARNESS_CONTINUITY.md +34 -0
- package/docs/DOCUMENTATION_GUIDE.md +31 -0
- package/docs/GETTING_STARTED.md +10 -0
- package/docs/MCP.md +126 -0
- package/docs/RECIPES.md +87 -1
- package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
- package/docs/TROUBLESHOOTING.md +243 -40
- package/docs/UNIVERSAL_INTEGRATION.md +48 -0
- package/package.json +17 -3
- package/schemas/execution.schema.json +11 -1
- package/schemas/task-recovery.schema.json +61 -0
- package/schemas/work-state.schema.json +1 -0
- package/src/cli.js +182 -337
- package/src/commands/audit.js +5 -0
- package/src/commands/doctor.js +22 -0
- package/src/commands/inspect.js +6 -0
- package/src/commands/migrate-protocol.js +18 -0
- package/src/commands/progress.js +6 -2
- package/src/commands/protocol-info.js +16 -0
- package/src/commands/run-check.js +2 -0
- package/src/commands/status.js +17 -0
- package/src/commands/task-create.js +42 -3
- package/src/commands/task-list.js +14 -1
- package/src/commands/task-lock-status.js +29 -0
- 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 +21 -7
- package/src/commands/task-unlock.js +8 -6
- package/src/commands/validate-protocol.js +19 -2
- package/src/core/artifact-registry.js +12 -0
- package/src/core/artifacts.js +17 -4
- package/src/core/audit.js +20 -4
- package/src/core/bundles.js +15 -0
- package/src/core/cli-command-definitions.js +94 -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 +17 -6
- package/src/core/completion-ownership.js +88 -0
- package/src/core/completion.js +15 -3
- package/src/core/diagnosis.js +15 -11
- package/src/core/error-codes.js +136 -1
- package/src/core/events.js +239 -9
- package/src/core/execution.js +73 -9
- package/src/core/filesystem.js +75 -8
- 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 +10 -3
- package/src/core/project-root.js +21 -0
- package/src/core/protocol-info.js +54 -0
- package/src/core/protocol-migration.js +59 -0
- package/src/core/reconcile-closure.js +54 -14
- package/src/core/recovery-history.js +116 -0
- package/src/core/resumability.js +8 -6
- package/src/core/schema-validation.js +1 -0
- package/src/core/task-claim-state.js +272 -0
- package/src/core/task-command.js +8 -4
- 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 +248 -18
- package/src/core/task-migration.js +24 -1
- package/src/core/task-paths.js +6 -3
- 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 +285 -0
- package/src/core/work-state.js +70 -6
- 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
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
+
}
|