@cassiomc1/forgeloop 1.9.0 → 1.10.1
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/AGENT_COMPATIBILITY.md +15 -0
- package/DELEGATION_PROTOCOL.md +6 -0
- package/DOCS_INDEX.md +13 -2
- package/LOOP_ENGINEERING.md +17 -4
- package/LOOP_SYSTEM_DESIGN.md +56 -0
- package/ORCHESTRATOR_INTEGRATION.md +9 -0
- package/PROTOCOL_INTEGRATION.md +31 -0
- package/QUALITY_SCORECARD.md +2 -2
- package/README.md +53 -0
- package/TERMINOLOGY.md +12 -0
- package/THREAT_MODEL.md +24 -0
- package/completions/_forgeloop +2 -1
- package/completions/forgeloop.bash +3 -1
- package/completions/forgeloop.fish +9 -1
- package/docs/ADVISORY_CONTEXT.md +174 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +28 -2
- package/docs/ARTIFACT_REFERENCE.md +29 -0
- package/docs/CLI_REFERENCE.md +40 -0
- package/docs/CODE_ATTESTATION.md +9 -0
- package/docs/CROSS_HARNESS_CONTINUITY.md +85 -0
- package/docs/DOCUMENTATION_GUIDE.md +11 -4
- package/docs/EXECUTION_PROFILE_BENCHMARKS.md +10 -0
- package/docs/GETTING_STARTED.md +22 -0
- package/docs/KNOWLEDGE_SOURCES.md +10 -0
- package/docs/MCP.md +29 -1
- package/docs/PACKAGE_CONTENTS.md +83 -0
- package/docs/RECIPES.md +80 -0
- package/docs/RELEASE_CHECKLIST.md +22 -0
- package/docs/REVISION_PROVIDERS.md +9 -0
- package/docs/TROUBLESHOOTING.md +66 -2
- package/docs/UNIVERSAL_INTEGRATION.md +60 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +10 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +22 -11
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +17 -8
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +14 -3
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +10 -1
- package/docs/diagrams/README.md +17 -0
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +383 -57
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +374 -55
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +328 -47
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/package.json +14 -4
- package/schemas/handoff-envelope.schema.json +1 -0
- package/scripts/CI_VALIDATORS.md +24 -0
- package/scripts/check-changelog-freshness.mjs +27 -3
- package/scripts/check-critical-coverage.mjs +9 -0
- package/scripts/generate-agent-protocol-summary.mjs +18 -0
- package/src/cli.js +6 -0
- package/src/commands/doctor.js +11 -10
- package/src/commands/handoff-accept.js +36 -0
- package/src/commands/handoff-list.js +28 -2
- package/src/commands/handoff-show.js +27 -2
- package/src/commands/reconcile-continuity.js +4 -0
- package/src/core/actions.js +2 -2
- package/src/core/advisory-context/constants.js +74 -0
- package/src/core/advisory-context/provider.js +287 -0
- package/src/core/advisory-context/service.js +140 -0
- package/src/core/approvals.js +2 -2
- package/src/core/artifacts.js +3 -3
- package/src/core/checks.js +0 -33
- package/src/core/cli-command-definitions.js +17 -0
- package/src/core/command-executors.js +12 -0
- package/src/core/command-input.js +11 -1
- package/src/core/completion.js +2 -2
- package/src/core/continuity-lint.js +89 -0
- package/src/core/continuity-reconciliation.js +16 -0
- package/src/core/continuity.js +10 -11
- package/src/core/error-codes.js +113 -0
- package/src/core/events.js +37 -5
- package/src/core/execution-profile-context.js +15 -1
- package/src/core/execution-profile.js +18 -5
- package/src/core/filesystem.js +18 -2
- package/src/core/handoff-acceptance.js +277 -0
- package/src/core/handoff.js +41 -8
- package/src/core/integration-invocation-policy.js +19 -2
- package/src/core/integration-resources.js +21 -1
- package/src/core/next-action-pending-actions.js +255 -0
- package/src/core/next-action-phases.js +26 -764
- package/src/core/next-action-planned-phase.js +51 -0
- package/src/core/next-action-quality-guidance.js +19 -0
- package/src/core/next-action-recovery-phases.js +97 -0
- package/src/core/next-action-refresh.js +20 -0
- package/src/core/next-action-review-phase.js +189 -0
- package/src/core/next-action-verification-phase.js +192 -0
- package/src/core/portable-context.js +103 -0
- package/src/core/protocol-info.js +18 -2
- package/src/core/runtime-context.js +31 -0
- package/src/core/task-recovery.js +2 -2
- package/src/core/transaction-maintenance.js +70 -0
- package/src/core/transaction.js +31 -10
- package/src/core/work-state.js +5 -5
- package/src/integration.d.ts +135 -3
- package/src/integration.js +23 -0
- package/src/core/cli-metadata.js +0 -23
- package/src/core/decision-classification.js +0 -55
- package/src/core/gates.js +0 -57
- package/src/core/workflow-compatibility.js +0 -151
|
@@ -160,11 +160,27 @@ export function protocolInfo({ packageVersion = null } = {}) {
|
|
|
160
160
|
rebinding: "EXPLICIT_ONLY",
|
|
161
161
|
},
|
|
162
162
|
canonicalHandoffs: {
|
|
163
|
-
version:
|
|
163
|
+
version: 2,
|
|
164
164
|
supported: true,
|
|
165
165
|
immutable: true,
|
|
166
|
-
actorControlledIntent: true,
|
|
167
166
|
lifecycleAuthority: false,
|
|
167
|
+
evidenceAuthority: false,
|
|
168
|
+
exactlyOnceAcceptance: true,
|
|
169
|
+
acceptanceLedgerBacked: true,
|
|
170
|
+
acceptanceCommand: "handoff-accept",
|
|
171
|
+
acceptanceStatuses: ["OPEN", "ACCEPTED", "UNBOUND", "INCONSISTENT"],
|
|
172
|
+
},
|
|
173
|
+
advisoryContextProviders: {
|
|
174
|
+
version: 1,
|
|
175
|
+
supported: true,
|
|
176
|
+
providerNeutral: true,
|
|
177
|
+
integrationApiOnly: true,
|
|
178
|
+
lazy: true,
|
|
179
|
+
optIn: true,
|
|
180
|
+
persistedByForgeLoop: false,
|
|
181
|
+
lifecycleAuthority: false,
|
|
182
|
+
evidenceAuthority: false,
|
|
183
|
+
executable: false,
|
|
168
184
|
},
|
|
169
185
|
responsibilityConstraints: {
|
|
170
186
|
version: 1,
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
normalizeVerificationExecutionPolicy,
|
|
5
5
|
} from "./verification-execution.js";
|
|
6
6
|
import { STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN } from "./structural-quality/constants.js";
|
|
7
|
+
import { E_ADVISORY_CONTEXT_PROVIDER_INVALID } from "./error-codes.js";
|
|
8
|
+
import { assertAdvisoryContextProviderIdentity } from "./advisory-context/provider.js";
|
|
7
9
|
|
|
8
10
|
export const AUTHORITY_TRUST_MODES = Object.freeze(["NONE", "HOST_ATTESTED"]);
|
|
9
11
|
|
|
@@ -132,5 +134,34 @@ export function createForgeLoopContext(options = {}) {
|
|
|
132
134
|
}
|
|
133
135
|
context.structuralQualityProviders = Object.freeze(providers);
|
|
134
136
|
}
|
|
137
|
+
if (options?.advisoryContextProviders !== undefined) {
|
|
138
|
+
const configured = options.advisoryContextProviders instanceof Map
|
|
139
|
+
? Object.fromEntries(options.advisoryContextProviders.entries())
|
|
140
|
+
: options.advisoryContextProviders;
|
|
141
|
+
if (!configured || typeof configured !== "object" || Array.isArray(configured)) {
|
|
142
|
+
const error = new Error("advisoryContextProviders must be an object or Map");
|
|
143
|
+
error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
const providers = {};
|
|
147
|
+
for (const [id, provider] of Object.entries(configured)) {
|
|
148
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(id)) {
|
|
149
|
+
const error = new Error(`Invalid advisory-context provider ID: ${id}`);
|
|
150
|
+
error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
if (typeof provider !== "function"
|
|
154
|
+
&& (!provider || typeof provider !== "object" || Array.isArray(provider))) {
|
|
155
|
+
const error = new Error(`Advisory-context provider ${id} must be an object or factory`);
|
|
156
|
+
error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
if (typeof provider !== "function") {
|
|
160
|
+
assertAdvisoryContextProviderIdentity(provider, id);
|
|
161
|
+
}
|
|
162
|
+
providers[id] = provider;
|
|
163
|
+
}
|
|
164
|
+
context.advisoryContextProviders = Object.freeze(providers);
|
|
165
|
+
}
|
|
135
166
|
return Object.freeze(context);
|
|
136
167
|
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
E_TASK_RECOVERY_AUTHORITY_INVALID,
|
|
6
6
|
} from "./error-codes.js";
|
|
7
7
|
import { taskArtifactPath } from "./task-paths.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getTaskTransaction } from "./transaction.js";
|
|
9
9
|
import { classifyRecoveryHistory } from "./recovery-history.js";
|
|
10
10
|
|
|
11
11
|
export const TASK_RECOVERY_SCHEMA_VERSION = 1;
|
|
@@ -86,7 +86,7 @@ export function writeTaskRecovery(target, recovery, packageRoot) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export async function clearTaskRecovery(target, { taskId } = {}) {
|
|
89
|
-
const transaction =
|
|
89
|
+
const transaction = (await getTaskTransaction(target));
|
|
90
90
|
if (!transaction) {
|
|
91
91
|
throw new Error("clearTaskRecovery requires an active task transaction");
|
|
92
92
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertSafePath, ensureWithin, fileExists, writeFileAtomic } from "./filesystem.js";
|
|
4
|
+
import { withTaskLock } from "./task-lock.js";
|
|
5
|
+
|
|
6
|
+
const ROOT = ".forgeloop/.txn";
|
|
7
|
+
const TERMINAL = new Set(["COMMITTED", "ROLLED_BACK", "ABORTED"]);
|
|
8
|
+
const DAY_MS = 86_400_000;
|
|
9
|
+
|
|
10
|
+
async function payloadBytes(directory) {
|
|
11
|
+
let bytes = 0;
|
|
12
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
13
|
+
const child = path.join(directory, entry.name);
|
|
14
|
+
const metadata = await lstat(child);
|
|
15
|
+
if (metadata.isSymbolicLink()) throw new Error("Transaction payload contains a symlink");
|
|
16
|
+
if (metadata.isDirectory()) bytes += await payloadBytes(child);
|
|
17
|
+
else if (metadata.isFile()) bytes += metadata.size;
|
|
18
|
+
else throw new Error("Transaction payload contains a special file");
|
|
19
|
+
}
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Compact terminal payloads only; preserve manifests and all event ledgers. */
|
|
24
|
+
export async function compactTransactions({ target, retainDays = 7, apply = false, now = Date.now() } = {}) {
|
|
25
|
+
if (!Number.isFinite(retainDays) || retainDays < 1) throw new Error("retainDays must be at least one day");
|
|
26
|
+
await assertSafePath(target, ROOT);
|
|
27
|
+
const root = ensureWithin(target, ROOT);
|
|
28
|
+
const report = { apply, scanned: 0, eligible: 0, bytes: 0, compacted: 0, skipped: [] };
|
|
29
|
+
if (!await fileExists(root)) return report;
|
|
30
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
31
|
+
if (!entry.isDirectory()) continue;
|
|
32
|
+
report.scanned += 1;
|
|
33
|
+
const relative = `${ROOT}/${entry.name}`;
|
|
34
|
+
const manifestPath = `${relative}/manifest.json`;
|
|
35
|
+
try {
|
|
36
|
+
await assertSafePath(target, manifestPath);
|
|
37
|
+
const original = JSON.parse(await readFile(ensureWithin(target, manifestPath), "utf8"));
|
|
38
|
+
const lockTaskId = original.lockTaskId ?? original.taskId;
|
|
39
|
+
if (typeof lockTaskId !== "string" || !lockTaskId) throw new Error("Missing lock identity");
|
|
40
|
+
await withTaskLock(target, lockTaskId, "compact-transactions", async () => {
|
|
41
|
+
const manifest = JSON.parse(await readFile(ensureWithin(target, manifestPath), "utf8"));
|
|
42
|
+
if ((manifest.lockTaskId ?? manifest.taskId) !== lockTaskId || manifest.transactionId !== entry.name) throw new Error("Changed transaction identity");
|
|
43
|
+
const timestamp = Date.parse(manifest.committedAt ?? manifest.recoveredAt ?? manifest.failedAt ?? manifest.startedAt);
|
|
44
|
+
if (!TERMINAL.has(manifest.status) || !Number.isFinite(timestamp) || now - timestamp < retainDays * DAY_MS) return;
|
|
45
|
+
const payloads = [];
|
|
46
|
+
let bytes = 0;
|
|
47
|
+
for (const name of ["stage", "backup"]) {
|
|
48
|
+
const payload = `${relative}/${name}`;
|
|
49
|
+
await assertSafePath(target, payload);
|
|
50
|
+
const fullPath = ensureWithin(target, payload);
|
|
51
|
+
if (await fileExists(fullPath)) {
|
|
52
|
+
bytes += await payloadBytes(fullPath);
|
|
53
|
+
payloads.push(fullPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!payloads.length) return;
|
|
57
|
+
report.eligible += 1;
|
|
58
|
+
report.bytes += bytes;
|
|
59
|
+
if (!apply) return;
|
|
60
|
+
for (const payload of payloads) await rm(payload, { recursive: true, force: true, maxRetries: 3 });
|
|
61
|
+
manifest.compactedAt = new Date(now).toISOString();
|
|
62
|
+
await writeFileAtomic(ensureWithin(target, manifestPath), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
63
|
+
report.compacted += 1;
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
report.skipped.push({ transactionId: entry.name, reason: error.message });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return report;
|
|
70
|
+
}
|
package/src/core/transaction.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
import { mkdir, open, readdir, readFile, rename, stat, truncate, unlink } from "node:fs/promises";
|
|
3
|
+
import { mkdir, open, readdir, readFile, realpath, rename, stat, truncate, unlink } from "node:fs/promises";
|
|
4
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
@@ -16,6 +16,18 @@ export function getActiveTaskTransaction() {
|
|
|
16
16
|
return transactionContext.getStore() ?? null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export async function getTaskTransaction(target) {
|
|
20
|
+
const transaction = getActiveTaskTransaction();
|
|
21
|
+
if (transaction && transaction.target !== await realpath(target)) {
|
|
22
|
+
const error = new Error("Cannot reuse a task transaction for a different project");
|
|
23
|
+
error.code = "E_TASK_CONTEXT_MISMATCH";
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
return transaction;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TERMINAL_STATUSES = new Set(["COMMITTED", "ROLLED_BACK", "ABORTED"]);
|
|
30
|
+
|
|
19
31
|
function transactionPath(transactionId) {
|
|
20
32
|
return `${TRANSACTION_ROOT}/${transactionId}`;
|
|
21
33
|
}
|
|
@@ -33,7 +45,7 @@ export async function findIncompleteTransactions(target) {
|
|
|
33
45
|
if (!entry.isDirectory()) continue;
|
|
34
46
|
try {
|
|
35
47
|
const manifest = JSON.parse(await readFile(path.join(root, entry.name, "manifest.json"), "utf8"));
|
|
36
|
-
if (manifest.status
|
|
48
|
+
if (!TERMINAL_STATUSES.has(manifest.status)) found.push(manifest);
|
|
37
49
|
} catch {
|
|
38
50
|
found.push({ transactionId: entry.name, status: "ABANDONED", malformed: true });
|
|
39
51
|
}
|
|
@@ -50,6 +62,7 @@ async function rollbackPublishedWrites(target, root, manifest) {
|
|
|
50
62
|
for (const entry of writes) {
|
|
51
63
|
if (!entry || typeof entry !== "object") continue;
|
|
52
64
|
const relativePath = writePath(entry);
|
|
65
|
+
await assertSafePath(target, relativePath);
|
|
53
66
|
if (entry.kind === "APPEND") {
|
|
54
67
|
if (!entry.appendStarted) continue;
|
|
55
68
|
const destination = ensureWithin(target, relativePath);
|
|
@@ -82,11 +95,15 @@ export async function recoverIncompleteTransactions(target) {
|
|
|
82
95
|
try {
|
|
83
96
|
const manifest = JSON.parse(await readFile(ensureWithin(target, manifestPath), "utf8"));
|
|
84
97
|
if (manifest.status !== "COMMITTING") continue;
|
|
85
|
-
await
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
await withTaskLock(target, manifest.lockTaskId ?? manifest.taskId ?? "transaction-recovery", "recover-transaction", async () => {
|
|
99
|
+
const current = JSON.parse(await readFile(ensureWithin(target, manifestPath), "utf8"));
|
|
100
|
+
if (current.status !== "COMMITTING") return;
|
|
101
|
+
await rollbackPublishedWrites(target, root, current);
|
|
102
|
+
current.status = "ROLLED_BACK";
|
|
103
|
+
current.recoveredAt = new Date().toISOString();
|
|
104
|
+
await writeManifest(target, manifestPath, current);
|
|
105
|
+
recovered.push({ transactionId: current.transactionId, status: current.status });
|
|
106
|
+
});
|
|
90
107
|
} catch (error) {
|
|
91
108
|
recovered.push({ transactionId: entry.name, status: "RECOVERY_FAILED", error: error.message });
|
|
92
109
|
}
|
|
@@ -103,7 +120,8 @@ export async function withTaskTransaction({
|
|
|
103
120
|
recordCommitEvent = false,
|
|
104
121
|
} = {}, callback) {
|
|
105
122
|
if (!target || !taskId) throw new Error("target and taskId are required for a task transaction");
|
|
106
|
-
|
|
123
|
+
target = await realpath(target);
|
|
124
|
+
const activeTransaction = await getTaskTransaction(target);
|
|
107
125
|
if (activeTransaction) {
|
|
108
126
|
if (activeTransaction.taskId !== taskId) {
|
|
109
127
|
throw new Error(`cannot nest task transaction for ${taskId} inside ${activeTransaction.taskId}`);
|
|
@@ -118,9 +136,10 @@ export async function withTaskTransaction({
|
|
|
118
136
|
const manifestPath = `${root}/manifest.json`;
|
|
119
137
|
await assertSafePath(target, manifestPath);
|
|
120
138
|
await mkdir(ensureWithin(target, stageRoot), { recursive: true });
|
|
121
|
-
const manifest = { schemaVersion: 1, transactionId, taskId, operation, lockId: lock.lockId, startedAt: new Date().toISOString(), status: "STAGING", writes: [] };
|
|
139
|
+
const manifest = { schemaVersion: 1, transactionId, taskId, lockTaskId, operation, lockId: lock.lockId, startedAt: new Date().toISOString(), status: "STAGING", writes: [] };
|
|
122
140
|
await writeManifest(target, manifestPath, manifest);
|
|
123
141
|
const tx = {
|
|
142
|
+
target,
|
|
124
143
|
transactionId,
|
|
125
144
|
taskId,
|
|
126
145
|
lock,
|
|
@@ -274,7 +293,9 @@ export async function withTaskTransaction({
|
|
|
274
293
|
manifest.rollbackError = { message: rollbackError.message };
|
|
275
294
|
}
|
|
276
295
|
} else {
|
|
277
|
-
|
|
296
|
+
// Staging has not published any writes; retain the diagnostic record
|
|
297
|
+
// without requiring recovery of a project that was never modified.
|
|
298
|
+
manifest.status = "ABORTED";
|
|
278
299
|
}
|
|
279
300
|
manifest.failedAt = new Date().toISOString();
|
|
280
301
|
manifest.error = { message: error.message };
|
package/src/core/work-state.js
CHANGED
|
@@ -19,7 +19,7 @@ import { assertJsonBytes } from "./json-safety.js";
|
|
|
19
19
|
import { assertCoverageList } from "./coverage.js";
|
|
20
20
|
import { canonicalFingerprint } from "./artifacts.js";
|
|
21
21
|
import { taskArtifactPath } from "./task-paths.js";
|
|
22
|
-
import {
|
|
22
|
+
import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
|
|
23
23
|
|
|
24
24
|
export const WORK_STATE_PATH = ".forgeloop/work-state.json";
|
|
25
25
|
|
|
@@ -245,7 +245,7 @@ export async function readWorkState(target, options = {}) {
|
|
|
245
245
|
const statePath = ensureWithin(target, relPath);
|
|
246
246
|
let state;
|
|
247
247
|
try {
|
|
248
|
-
const transaction =
|
|
248
|
+
const transaction = (await getTaskTransaction(target));
|
|
249
249
|
const staged = transaction ? await transaction.readText(relPath) : null;
|
|
250
250
|
if (staged === null) {
|
|
251
251
|
if (!(await fileExists(statePath))) return null;
|
|
@@ -290,7 +290,7 @@ export async function writeWorkState(target, state, options = {}) {
|
|
|
290
290
|
await validateStoredState(state, packageRoot);
|
|
291
291
|
await assertSafePath(target, relPath);
|
|
292
292
|
const serialized = `${JSON.stringify(state, null, 2)}\n`;
|
|
293
|
-
const transaction =
|
|
293
|
+
const transaction = (await getTaskTransaction(target));
|
|
294
294
|
if (!dryRun && transaction) {
|
|
295
295
|
await transaction.stageText(relPath, serialized);
|
|
296
296
|
} else {
|
|
@@ -306,7 +306,7 @@ export async function mutateWorkState(target, { expectedRevision, packageRoot =
|
|
|
306
306
|
error.code = "E_STATE_REVISION_CONFLICT";
|
|
307
307
|
throw error;
|
|
308
308
|
}
|
|
309
|
-
if (!
|
|
309
|
+
if (!(await getTaskTransaction(target))) {
|
|
310
310
|
return withTaskTransaction({
|
|
311
311
|
target,
|
|
312
312
|
taskId: taskId ?? "legacy-work-state",
|
|
@@ -331,7 +331,7 @@ export async function mutateWorkState(target, { expectedRevision, packageRoot =
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
export async function initializeWorkState(target, state, { packageRoot = getPackageRoot(), taskId, statePath } = {}) {
|
|
334
|
-
if (
|
|
334
|
+
if ((await getTaskTransaction(target))) {
|
|
335
335
|
const current = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
336
336
|
if (current) return current;
|
|
337
337
|
await writeWorkState(target, state, { packageRoot, taskId, statePath });
|
package/src/integration.d.ts
CHANGED
|
@@ -61,6 +61,65 @@ export interface ForgeLoopContext {
|
|
|
61
61
|
verificationExecutionPolicy?: { requiredIsolation: string };
|
|
62
62
|
usageProvider?: ForgeLoopUsageProvider;
|
|
63
63
|
structuralQualityProviders?: Readonly<Record<string, ForgeLoopStructuralQualityProvider | ForgeLoopStructuralQualityProviderFactory>>;
|
|
64
|
+
advisoryContextProviders?: Readonly<Record<string, ForgeLoopAdvisoryContextProvider | ForgeLoopAdvisoryContextProviderFactory>>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ForgeLoopAdvisoryContextItem {
|
|
68
|
+
title?: string;
|
|
69
|
+
summary: string;
|
|
70
|
+
sourceRef?: string;
|
|
71
|
+
observedAt?: string;
|
|
72
|
+
confidence?: number;
|
|
73
|
+
itemFingerprint?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ForgeLoopAdvisoryContextRecallInput {
|
|
77
|
+
projectPath: string;
|
|
78
|
+
taskId: string;
|
|
79
|
+
query: string;
|
|
80
|
+
limit?: number;
|
|
81
|
+
maxItemChars?: number;
|
|
82
|
+
maxTotalChars?: number;
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ForgeLoopAdvisoryContextRecallOutput {
|
|
87
|
+
items: Array<{
|
|
88
|
+
title?: string;
|
|
89
|
+
summary: string;
|
|
90
|
+
sourceRef?: string;
|
|
91
|
+
observedAt?: string;
|
|
92
|
+
confidence?: number;
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ForgeLoopAdvisoryRecallOptions {
|
|
97
|
+
limit: number;
|
|
98
|
+
maxItemChars: number;
|
|
99
|
+
maxTotalChars: number;
|
|
100
|
+
timeoutMs: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ForgeLoopAdvisoryContextProvider {
|
|
104
|
+
id: string;
|
|
105
|
+
version?: string;
|
|
106
|
+
recall(input: ForgeLoopAdvisoryContextRecallInput): Promise<ForgeLoopAdvisoryContextRecallOutput> | ForgeLoopAdvisoryContextRecallOutput;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type ForgeLoopAdvisoryContextProviderFactory = () => ForgeLoopAdvisoryContextProvider | Promise<ForgeLoopAdvisoryContextProvider>;
|
|
110
|
+
|
|
111
|
+
export interface ForgeLoopNormalizedAdvisoryContextResult {
|
|
112
|
+
provider: {
|
|
113
|
+
id: string;
|
|
114
|
+
version?: string;
|
|
115
|
+
};
|
|
116
|
+
taskId: string | null;
|
|
117
|
+
authority: "ADVISORY";
|
|
118
|
+
evidenceAuthority: "NONE";
|
|
119
|
+
actionability: "NON_EXECUTABLE";
|
|
120
|
+
trustRole: "NON_EVIDENCE_ADVISORY_CONTEXT";
|
|
121
|
+
persisted: false;
|
|
122
|
+
items: readonly ForgeLoopAdvisoryContextItem[];
|
|
64
123
|
}
|
|
65
124
|
|
|
66
125
|
export interface ForgeLoopStructuralQualityProviderInput {
|
|
@@ -70,11 +129,12 @@ export interface ForgeLoopStructuralQualityProviderInput {
|
|
|
70
129
|
maxOutputBytes: number;
|
|
71
130
|
}
|
|
72
131
|
|
|
73
|
-
export
|
|
74
|
-
|
|
132
|
+
export type ForgeLoopStructuralQualityProvider = { id: string } & ({
|
|
133
|
+
observe(input: ForgeLoopStructuralQualityProviderInput): Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
134
|
+
} | {
|
|
75
135
|
detect(input: ForgeLoopStructuralQualityProviderInput): Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
76
136
|
scan(input: ForgeLoopStructuralQualityProviderInput): Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
77
|
-
}
|
|
137
|
+
});
|
|
78
138
|
|
|
79
139
|
export type ForgeLoopStructuralQualityProviderFactory = (input: ForgeLoopStructuralQualityProviderInput) => ForgeLoopStructuralQualityProvider | Promise<ForgeLoopStructuralQualityProvider>;
|
|
80
140
|
|
|
@@ -198,3 +258,75 @@ export declare function classifyForgeLoopInvocation(command: string, input?: For
|
|
|
198
258
|
export declare function readForgeLoopIntegrationResource(uri: string, input?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
199
259
|
export declare function resolveForgeLoopProjectRoot(projectPath?: string, input?: { cwd?: string }): Promise<string>;
|
|
200
260
|
export declare function createForgeLoopContext(input?: Record<string, unknown>): ForgeLoopContext;
|
|
261
|
+
export declare function recallAdvisoryContext(input: {
|
|
262
|
+
target: string;
|
|
263
|
+
taskId: string;
|
|
264
|
+
providerName: string;
|
|
265
|
+
query: string;
|
|
266
|
+
limit?: number;
|
|
267
|
+
maxItemChars?: number;
|
|
268
|
+
maxTotalChars?: number;
|
|
269
|
+
timeoutMs?: number;
|
|
270
|
+
runtimeContext?: ForgeLoopContext | Record<string, unknown>;
|
|
271
|
+
}): Promise<ForgeLoopNormalizedAdvisoryContextResult>;
|
|
272
|
+
export declare const ADVISORY_CONTEXT_LIMITS: Readonly<Record<string, number>>;
|
|
273
|
+
export declare const ADVISORY_CONTEXT_TRUST: Readonly<Record<string, unknown>>;
|
|
274
|
+
export declare function normalizeAdvisoryRecallOptions(input?: Partial<ForgeLoopAdvisoryRecallOptions>): ForgeLoopAdvisoryRecallOptions;
|
|
275
|
+
export declare function assertAdvisoryContextProvider(provider: unknown): ForgeLoopAdvisoryContextProvider;
|
|
276
|
+
export declare function assertAdvisoryContextProviderIdentity(provider: unknown, expectedId: string): ForgeLoopAdvisoryContextProvider;
|
|
277
|
+
export declare function createAdvisoryContextProviderRegistry(options?: { providers?: Record<string, unknown> }): {
|
|
278
|
+
get(id: string): unknown;
|
|
279
|
+
has(id: string): boolean;
|
|
280
|
+
list(): string[];
|
|
281
|
+
};
|
|
282
|
+
export declare function resolveAdvisoryContextProvider(options: {
|
|
283
|
+
providers?: Record<string, unknown> | Map<string, unknown> | { get(id: string): unknown; has(id: string): boolean };
|
|
284
|
+
providerName: string;
|
|
285
|
+
}): Promise<ForgeLoopAdvisoryContextProvider | null>;
|
|
286
|
+
export declare function normalizeAdvisoryContextResult(raw: unknown, options?: Record<string, unknown>): ForgeLoopNormalizedAdvisoryContextResult;
|
|
287
|
+
export declare function normalizePortableText(value: unknown, options?: { label?: string; maxLength?: number; optional?: boolean }): string | null;
|
|
288
|
+
export declare function assertPortableContextSafe<T = unknown>(value: T, options?: { label?: string }): T;
|
|
289
|
+
export declare class PortableContextError extends Error {
|
|
290
|
+
code: string;
|
|
291
|
+
}
|
|
292
|
+
export declare function acceptCanonicalHandoff(target: string, options: {
|
|
293
|
+
taskId: string;
|
|
294
|
+
handoffId: string;
|
|
295
|
+
consumerId: string;
|
|
296
|
+
harness?: string | null;
|
|
297
|
+
packageRoot?: string;
|
|
298
|
+
}): Promise<{
|
|
299
|
+
accepted: boolean;
|
|
300
|
+
idempotent: boolean;
|
|
301
|
+
handoffId: string;
|
|
302
|
+
consumerId: string;
|
|
303
|
+
harness: string | null;
|
|
304
|
+
acceptedAt: string;
|
|
305
|
+
}>;
|
|
306
|
+
export declare function resolveHandoffAcceptance(input: {
|
|
307
|
+
events?: readonly unknown[];
|
|
308
|
+
handoff: unknown;
|
|
309
|
+
ledgerValid?: boolean;
|
|
310
|
+
ledgerErrors?: readonly ({ code?: string } | string)[];
|
|
311
|
+
}): {
|
|
312
|
+
status: "OPEN" | "ACCEPTED" | "INCONSISTENT" | "UNBOUND";
|
|
313
|
+
consumerId?: string;
|
|
314
|
+
harness?: string;
|
|
315
|
+
acceptedAt?: string;
|
|
316
|
+
reasonCodes?: readonly string[];
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export declare const E_VERIFICATION_EXECUTION_INVALID: "E_VERIFICATION_EXECUTION_INVALID";
|
|
320
|
+
export declare const E_VERIFICATION_ISOLATION_UNAVAILABLE: "E_VERIFICATION_ISOLATION_UNAVAILABLE";
|
|
321
|
+
export declare function createStructuralQualityProviderRegistry(options?: {
|
|
322
|
+
providers?: Readonly<Record<string, ForgeLoopStructuralQualityProvider | ForgeLoopStructuralQualityProviderFactory>>;
|
|
323
|
+
builtIns?: Readonly<Record<string, ForgeLoopStructuralQualityProvider | ForgeLoopStructuralQualityProviderFactory>>;
|
|
324
|
+
}): { resolve(name: string, input: ForgeLoopStructuralQualityProviderInput): Promise<ForgeLoopStructuralQualityProvider> };
|
|
325
|
+
export declare function resolveStructuralQualityProvider(options: {
|
|
326
|
+
providerName?: string;
|
|
327
|
+
target: string;
|
|
328
|
+
taskId: string;
|
|
329
|
+
timeoutMs?: number;
|
|
330
|
+
maxOutputBytes?: number;
|
|
331
|
+
runtimeContext?: ForgeLoopContext;
|
|
332
|
+
}): Promise<ForgeLoopStructuralQualityProvider>;
|
package/src/integration.js
CHANGED
|
@@ -31,6 +31,7 @@ export function getForgeLoopPackageVersion() {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export {
|
|
34
|
+
FORGELOOP_INTEGRATION_RUNTIME_VERSION,
|
|
34
35
|
executeForgeLoopCommand,
|
|
35
36
|
validateForgeLoopCommandInput,
|
|
36
37
|
defaultCommandInputValues,
|
|
@@ -79,5 +80,27 @@ export {
|
|
|
79
80
|
VERIFICATION_EXECUTION_POLICY_MODES,
|
|
80
81
|
VERIFICATION_ISOLATION_MODES,
|
|
81
82
|
} from "./core/verification-execution.js";
|
|
83
|
+
export { recallAdvisoryContext } from "./core/advisory-context/service.js";
|
|
84
|
+
export {
|
|
85
|
+
ADVISORY_CONTEXT_LIMITS,
|
|
86
|
+
ADVISORY_CONTEXT_TRUST,
|
|
87
|
+
normalizeAdvisoryRecallOptions,
|
|
88
|
+
} from "./core/advisory-context/constants.js";
|
|
89
|
+
export {
|
|
90
|
+
assertAdvisoryContextProvider,
|
|
91
|
+
assertAdvisoryContextProviderIdentity,
|
|
92
|
+
createAdvisoryContextProviderRegistry,
|
|
93
|
+
normalizeAdvisoryContextResult,
|
|
94
|
+
resolveAdvisoryContextProvider,
|
|
95
|
+
} from "./core/advisory-context/provider.js";
|
|
96
|
+
export {
|
|
97
|
+
normalizePortableText,
|
|
98
|
+
assertPortableContextSafe,
|
|
99
|
+
PortableContextError,
|
|
100
|
+
} from "./core/portable-context.js";
|
|
101
|
+
export {
|
|
102
|
+
acceptCanonicalHandoff,
|
|
103
|
+
resolveHandoffAcceptance,
|
|
104
|
+
} from "./core/handoff-acceptance.js";
|
|
82
105
|
|
|
83
106
|
export const FORGELOOP_INTEGRATION_API_VERSION = FORGELOOP_INTEGRATION_RUNTIME_VERSION;
|
package/src/core/cli-metadata.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Canonical metadata for all 43 ForgeLoop CLI commands derived directly
|
|
5
|
-
* from CLI_COMMAND_DEFINITIONS to guarantee zero divergence.
|
|
6
|
-
*/
|
|
7
|
-
export const CLI_COMMAND_METADATA = Object.freeze(
|
|
8
|
-
Object.fromEntries(
|
|
9
|
-
Object.entries(CLI_COMMAND_DEFINITIONS).map(([name, def]) => [
|
|
10
|
-
name,
|
|
11
|
-
Object.freeze({
|
|
12
|
-
name: def.name,
|
|
13
|
-
category: def.category,
|
|
14
|
-
mutation: def.mutation,
|
|
15
|
-
options: Object.keys(def.options),
|
|
16
|
-
writes: def.writes,
|
|
17
|
-
removes: def.removes,
|
|
18
|
-
mayExecuteExternalProcess: def.mayExecuteExternalProcess,
|
|
19
|
-
description: def.description,
|
|
20
|
-
}),
|
|
21
|
-
]),
|
|
22
|
-
),
|
|
23
|
-
);
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
export const BLOCKING_REASON_CODES = Object.freeze([
|
|
2
|
-
"REAL_BUSINESS_FACT_REQUIRED",
|
|
3
|
-
"SENSITIVE_VALUE_REQUIRED",
|
|
4
|
-
"EXTERNAL_AUTHORITY_REQUIRED",
|
|
5
|
-
"IRREVERSIBLE_DECISION_REQUIRED",
|
|
6
|
-
"REGULATED_CLAIM_REQUIRED",
|
|
7
|
-
"DESTRUCTIVE_ACTION_REQUIRED",
|
|
8
|
-
]);
|
|
9
|
-
|
|
10
|
-
const BLOCKING_RULES = Object.freeze([
|
|
11
|
-
["realBusinessFact", "REAL_BUSINESS_FACT_REQUIRED"],
|
|
12
|
-
["sensitive", "SENSITIVE_VALUE_REQUIRED"],
|
|
13
|
-
["authoritative", "EXTERNAL_AUTHORITY_REQUIRED"],
|
|
14
|
-
["external", "EXTERNAL_AUTHORITY_REQUIRED"],
|
|
15
|
-
["irreversible", "IRREVERSIBLE_DECISION_REQUIRED"],
|
|
16
|
-
["regulatedClaim", "REGULATED_CLAIM_REQUIRED"],
|
|
17
|
-
["destructive", "DESTRUCTIVE_ACTION_REQUIRED"],
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
function blockingDecision(reasonCode) {
|
|
21
|
-
return {
|
|
22
|
-
classification: "BLOCKING",
|
|
23
|
-
reasonCode,
|
|
24
|
-
blockingReason: reasonCode,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function classifyDecision(flags = {}) {
|
|
29
|
-
if (!flags || typeof flags !== "object" || Array.isArray(flags)) {
|
|
30
|
-
return { classification: "NON_BLOCKING", reasonCode: "SAFE_REVERSIBLE_LOCAL_DEFAULT", blockingReason: null };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
for (const [flag, reasonCode] of BLOCKING_RULES) {
|
|
34
|
-
if (flags[flag] === true) return blockingDecision(reasonCode);
|
|
35
|
-
}
|
|
36
|
-
if (flags.reversible === false || flags.local === false) {
|
|
37
|
-
return blockingDecision(
|
|
38
|
-
flags.reversible === false
|
|
39
|
-
? "IRREVERSIBLE_DECISION_REQUIRED"
|
|
40
|
-
: "REAL_BUSINESS_FACT_REQUIRED",
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
classification: "NON_BLOCKING",
|
|
46
|
-
reasonCode: "SAFE_REVERSIBLE_LOCAL_DEFAULT",
|
|
47
|
-
blockingReason: null,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function canAskUser(decision) {
|
|
52
|
-
return decision?.classification === "BLOCKING"
|
|
53
|
-
&& BLOCKING_REASON_CODES.includes(decision.reasonCode)
|
|
54
|
-
&& decision.blockingReason === decision.reasonCode;
|
|
55
|
-
}
|
package/src/core/gates.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
export const GATE_SCHEMA_VERSION = 1;
|
|
5
|
-
export const GATE_STATUSES = Object.freeze(["satisfied", "unverified", "blocked"]);
|
|
6
|
-
|
|
7
|
-
function stringArray(value, label) {
|
|
8
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
9
|
-
throw new Error(`${label} must be an array of non-empty strings`);
|
|
10
|
-
}
|
|
11
|
-
return [...value];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function artifactList(value) {
|
|
15
|
-
if (!Array.isArray(value)) throw new Error("Gate artifacts must be an array");
|
|
16
|
-
return value.map((artifact) => {
|
|
17
|
-
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) {
|
|
18
|
-
throw new Error("Gate artifact entries must be objects");
|
|
19
|
-
}
|
|
20
|
-
if (typeof artifact.path !== "string" || !artifact.path) throw new Error("Gate artifact path is required");
|
|
21
|
-
const portable = artifact.path.replaceAll("\\", "/");
|
|
22
|
-
const normalized = path.posix.normalize(portable);
|
|
23
|
-
if (portable.startsWith("/") || /^[A-Za-z]:\//.test(portable) || normalized === ".." || normalized.startsWith("../")) {
|
|
24
|
-
throw new Error(`Gate artifact path escapes the target: ${artifact.path}`);
|
|
25
|
-
}
|
|
26
|
-
if (typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(artifact.sha256)) {
|
|
27
|
-
throw new Error(`Gate artifact hash is invalid: ${artifact.path}`);
|
|
28
|
-
}
|
|
29
|
-
return { path: normalized.replace(/^\.\//, ""), sha256: artifact.sha256 };
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function createGate(input = {}) {
|
|
34
|
-
if (typeof input.taskId !== "string" || !input.taskId) throw new Error("Gate taskId is required");
|
|
35
|
-
if (typeof input.gate !== "string" || !input.gate) throw new Error("Gate name is required");
|
|
36
|
-
if (!GATE_STATUSES.includes(input.status)) throw new Error(`Unknown gate status: ${input.status}`);
|
|
37
|
-
const artifacts = artifactList(input.artifacts ?? []);
|
|
38
|
-
return {
|
|
39
|
-
schemaVersion: GATE_SCHEMA_VERSION,
|
|
40
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
41
|
-
taskId: input.taskId,
|
|
42
|
-
gate: input.gate,
|
|
43
|
-
status: input.status,
|
|
44
|
-
requiredBy: stringArray(input.requiredBy ?? [], "Gate requiredBy"),
|
|
45
|
-
artifacts,
|
|
46
|
-
decisions: stringArray(input.decisions ?? [], "Gate decisions"),
|
|
47
|
-
unknowns: stringArray(input.unknowns ?? [], "Gate unknowns"),
|
|
48
|
-
approvedAssumptions: stringArray(input.approvedAssumptions ?? [], "Gate approvedAssumptions"),
|
|
49
|
-
evidence: Array.isArray(input.evidence) ? input.evidence.map((item) => ({ ...item })) : [],
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
import { taskGatePath } from "./task-paths.js";
|
|
54
|
-
|
|
55
|
-
export function requiredGatePaths(gates, { taskId } = {}) {
|
|
56
|
-
return [...new Set(gates)].sort().map((gate) => (taskId ? taskGatePath(taskId, gate) : `.forgeloop/gates/${gate}.json`));
|
|
57
|
-
}
|