@cassiomc1/forgeloop 1.0.0 → 1.1.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/.cursor/rules/project-loop.mdc +3 -2
- package/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +2 -1
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +36 -0
- package/ENG/design-code-eng.md +31 -0
- package/ENG/documentation-quality-eng.md +400 -0
- package/EXECUTION_STATE.md +23 -13
- package/GUIDE_ROUTER.md +23 -8
- package/LOOP_ENGINEERING.md +77 -12
- package/PROTOCOL_INTEGRATION.md +9 -6
- package/README.md +82 -39
- package/docs/ARTIFACT_REFERENCE.md +442 -0
- package/docs/CLI_REFERENCE.md +892 -0
- package/docs/CROSS_HARNESS_CONTINUITY.md +198 -0
- package/docs/DOCUMENTATION_GUIDE.md +161 -0
- package/docs/GETTING_STARTED.md +348 -0
- package/docs/RECIPES.md +250 -0
- package/docs/TROUBLESHOOTING.md +345 -0
- package/docs/assets/forgeloop-flow.svg +1 -1
- package/docs/forgeloop-flow.mmd +1 -1
- package/package.json +13 -2
- package/schemas/task-descriptor.schema.json +56 -0
- package/schemas/work-state.schema.json +18 -1
- package/scripts/CI_VALIDATORS.md +7 -0
- package/src/cli.js +280 -388
- package/src/commands/advance.js +5 -2
- package/src/commands/audit.js +11 -1
- package/src/commands/clear-continuity.js +5 -2
- package/src/commands/clear-state.js +5 -2
- package/src/commands/complete.js +9 -1
- package/src/commands/continuity.js +5 -2
- package/src/commands/inspect.js +10 -2
- package/src/commands/next.js +5 -2
- package/src/commands/preflight.js +9 -1
- package/src/commands/prepare-completion.js +5 -2
- package/src/commands/reconcile-continuity.js +5 -2
- package/src/commands/record-check.js +7 -1
- package/src/commands/record-continuity.js +21 -14
- package/src/commands/record-terminal-result.js +7 -1
- package/src/commands/route.js +22 -18
- package/src/commands/run-check.js +52 -44
- package/src/commands/status.js +18 -12
- package/src/commands/task-create.js +94 -0
- package/src/commands/task-list.js +48 -0
- package/src/commands/task-migrate.js +34 -0
- package/src/commands/task-scope.js +75 -0
- package/src/commands/task-show.js +81 -0
- package/src/commands/task-unlock.js +35 -0
- package/src/commands/validate-protocol.js +37 -20
- package/src/commands/validate-state.js +24 -18
- package/src/config/guides.json +42 -0
- package/src/core/activation.js +8 -4
- package/src/core/artifact-registry.js +166 -0
- package/src/core/audit.js +65 -12
- package/src/core/bundles.js +76 -50
- package/src/core/cli-command-definitions.js +611 -0
- package/src/core/cli-metadata.js +23 -0
- package/src/core/completion-artifacts.js +161 -74
- package/src/core/completion.js +134 -76
- package/src/core/continuity.js +20 -13
- package/src/core/contract.js +6 -3
- package/src/core/error-codes.js +197 -0
- package/src/core/events.js +19 -14
- package/src/core/execution.js +38 -6
- package/src/core/gate-artifact.js +12 -9
- package/src/core/gates.js +4 -2
- package/src/core/guide-metadata.js +7 -11
- package/src/core/guide-registry.js +29 -0
- package/src/core/inspect.js +7 -4
- package/src/core/native-adapters.js +6 -0
- package/src/core/phase.js +85 -33
- package/src/core/preflight-consistency.js +24 -14
- package/src/core/preflight-loaders.js +16 -11
- package/src/core/preflight.js +44 -25
- package/src/core/protocol.js +2 -11
- package/src/core/receipt.js +1 -1
- package/src/core/report.js +2 -2
- package/src/core/repository.js +46 -12
- package/src/core/resumability.js +6 -4
- package/src/core/route-artifact.js +9 -5
- package/src/core/router.js +11 -7
- package/src/core/schema-validation.js +1 -0
- package/src/core/task-command.js +41 -0
- package/src/core/task-context.js +126 -0
- package/src/core/task-descriptor.js +81 -0
- package/src/core/task-discovery.js +116 -0
- package/src/core/task-identity.js +76 -0
- package/src/core/task-lock.js +209 -0
- package/src/core/task-migration-validation.js +140 -0
- package/src/core/task-migration.js +361 -0
- package/src/core/task-paths.js +96 -0
- package/src/core/task-scope.js +179 -0
- package/src/core/templates.js +3 -9
- package/src/core/work-state.js +24 -13
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { FAILURE_CODES } from "./protocol.js";
|
|
2
|
+
import {
|
|
3
|
+
E_AUTHORITY_INVALID,
|
|
4
|
+
E_AUTHORITY_SCOPE_MISMATCH,
|
|
5
|
+
E_COMMAND_RESOLUTION_AMBIGUOUS,
|
|
6
|
+
E_INSTALLATION_AUTHORITY_REQUIRED,
|
|
7
|
+
E_VERIFICATION_TOOL_UNAVAILABLE,
|
|
8
|
+
} from "./verification-constants.js";
|
|
9
|
+
|
|
10
|
+
export const E_TASK_REQUIRED = "E_TASK_REQUIRED";
|
|
11
|
+
export const E_TASK_NOT_FOUND = "E_TASK_NOT_FOUND";
|
|
12
|
+
export const E_TASK_ALREADY_EXISTS = "E_TASK_ALREADY_EXISTS";
|
|
13
|
+
export const E_TASK_AMBIGUOUS = "E_TASK_AMBIGUOUS";
|
|
14
|
+
export const E_TASK_SELECTOR_CONFLICT = "E_TASK_SELECTOR_CONFLICT";
|
|
15
|
+
export const E_TASK_DESCRIPTOR_INVALID = "E_TASK_DESCRIPTOR_INVALID";
|
|
16
|
+
export const E_TASK_KEY_MISMATCH = "E_TASK_KEY_MISMATCH";
|
|
17
|
+
export const E_TASK_CONTEXT_MISMATCH = "E_TASK_CONTEXT_MISMATCH";
|
|
18
|
+
|
|
19
|
+
export const E_TASK_LOCKED = "E_TASK_LOCKED";
|
|
20
|
+
export const E_TASK_LOCK_INVALID = "E_TASK_LOCK_INVALID";
|
|
21
|
+
|
|
22
|
+
export const E_TASK_SCOPE_REQUIRED = "E_TASK_SCOPE_REQUIRED";
|
|
23
|
+
export const E_TASK_SCOPE_CONFLICT = "E_TASK_SCOPE_CONFLICT";
|
|
24
|
+
export const E_TASK_SCOPE_DIRTY = "E_TASK_SCOPE_DIRTY";
|
|
25
|
+
export const E_TASK_SCOPE_FROZEN = "E_TASK_SCOPE_FROZEN";
|
|
26
|
+
export const E_TASK_CHANGE_OUTSIDE_SCOPE = "E_TASK_CHANGE_OUTSIDE_SCOPE";
|
|
27
|
+
export const E_TASK_CHANGE_ATTRIBUTION_UNAVAILABLE = "E_TASK_CHANGE_ATTRIBUTION_UNAVAILABLE";
|
|
28
|
+
|
|
29
|
+
export const E_TASK_LAYOUT_LEGACY = "E_TASK_LAYOUT_LEGACY";
|
|
30
|
+
export const E_TASK_MIGRATION_INVALID = "E_TASK_MIGRATION_INVALID";
|
|
31
|
+
export const E_TASK_MIGRATION_IDENTITY_MISMATCH = "E_TASK_MIGRATION_IDENTITY_MISMATCH";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Public, stable ForgeLoop error and reason codes documented for users and harnesses.
|
|
35
|
+
*/
|
|
36
|
+
export const PUBLIC_ERROR_CODES = Object.freeze({
|
|
37
|
+
E_PREFLIGHT_NOT_READY: Object.freeze({
|
|
38
|
+
code: "E_PREFLIGHT_NOT_READY",
|
|
39
|
+
category: "preflight",
|
|
40
|
+
classification: "PUBLIC_STABLE",
|
|
41
|
+
meaning: "Preflight gates or contract validations are incomplete.",
|
|
42
|
+
safeResolution: "Satisfy required gates and check preflight output.",
|
|
43
|
+
}),
|
|
44
|
+
E_CONTRACT_STALE: Object.freeze({
|
|
45
|
+
code: "E_CONTRACT_STALE",
|
|
46
|
+
category: "freshness",
|
|
47
|
+
classification: "PUBLIC_STABLE",
|
|
48
|
+
meaning: "Contract modified after downstream artifacts were generated.",
|
|
49
|
+
safeResolution: "Re-run forgeloop route and forgeloop preflight.",
|
|
50
|
+
}),
|
|
51
|
+
E_ROUTE_STALE: Object.freeze({
|
|
52
|
+
code: "E_ROUTE_STALE",
|
|
53
|
+
category: "freshness",
|
|
54
|
+
classification: "PUBLIC_STABLE",
|
|
55
|
+
meaning: "Routing result does not match the active contract fingerprint.",
|
|
56
|
+
safeResolution: "Re-run forgeloop route.",
|
|
57
|
+
}),
|
|
58
|
+
E_GATE_STALE: Object.freeze({
|
|
59
|
+
code: "E_GATE_STALE",
|
|
60
|
+
category: "freshness",
|
|
61
|
+
classification: "PUBLIC_STABLE",
|
|
62
|
+
meaning: "Referenced gate artifact changed after approval.",
|
|
63
|
+
safeResolution: "Update artifact SHA-256 in gate file.",
|
|
64
|
+
}),
|
|
65
|
+
E_VERIFICATION_TOOL_UNAVAILABLE: Object.freeze({
|
|
66
|
+
code: "E_VERIFICATION_TOOL_UNAVAILABLE",
|
|
67
|
+
category: "capability",
|
|
68
|
+
classification: "PUBLIC_STABLE",
|
|
69
|
+
meaning: "Required verification executable is missing in environment.",
|
|
70
|
+
safeResolution: "Use local equivalent, obtain host authority, or record NOT_VERIFIED.",
|
|
71
|
+
}),
|
|
72
|
+
E_INSTALLATION_AUTHORITY_REQUIRED: Object.freeze({
|
|
73
|
+
code: "E_INSTALLATION_AUTHORITY_REQUIRED",
|
|
74
|
+
category: "authority",
|
|
75
|
+
classification: "PUBLIC_STABLE",
|
|
76
|
+
meaning: "Attempted software installation without host authority grant.",
|
|
77
|
+
safeResolution: "Use local non-installing binaries or request host authority grant.",
|
|
78
|
+
}),
|
|
79
|
+
E_AUTHORITY_INVALID: Object.freeze({
|
|
80
|
+
code: "E_AUTHORITY_INVALID",
|
|
81
|
+
category: "authority",
|
|
82
|
+
classification: "PUBLIC_STABLE",
|
|
83
|
+
meaning: "Authority grant file is malformed or expired.",
|
|
84
|
+
safeResolution: "Obtain a valid authority grant from host operator.",
|
|
85
|
+
}),
|
|
86
|
+
E_AUTHORITY_SCOPE_MISMATCH: Object.freeze({
|
|
87
|
+
code: "E_AUTHORITY_SCOPE_MISMATCH",
|
|
88
|
+
category: "authority",
|
|
89
|
+
classification: "PUBLIC_STABLE",
|
|
90
|
+
meaning: "Authority grant does not cover the requested package.",
|
|
91
|
+
safeResolution: "Request updated authority scope.",
|
|
92
|
+
}),
|
|
93
|
+
E_AUTHORITY_UNTRUSTED_SOURCE: Object.freeze({
|
|
94
|
+
code: "E_AUTHORITY_UNTRUSTED_SOURCE",
|
|
95
|
+
category: "authority",
|
|
96
|
+
classification: "PUBLIC_STABLE",
|
|
97
|
+
meaning: "Authority file placed inside untrusted project tree.",
|
|
98
|
+
safeResolution: "Place authority file in host-managed trusted location.",
|
|
99
|
+
}),
|
|
100
|
+
E_EXECUTION_REF_INVALID: Object.freeze({
|
|
101
|
+
code: "E_EXECUTION_REF_INVALID",
|
|
102
|
+
category: "provenance",
|
|
103
|
+
classification: "PUBLIC_STABLE",
|
|
104
|
+
meaning: "Referenced execution ID does not exist.",
|
|
105
|
+
safeResolution: "Re-run check via forgeloop run-check.",
|
|
106
|
+
}),
|
|
107
|
+
E_CHECK_INVALID: Object.freeze({
|
|
108
|
+
code: "E_CHECK_INVALID",
|
|
109
|
+
category: "verification",
|
|
110
|
+
classification: "PUBLIC_STABLE",
|
|
111
|
+
meaning: "Check structure or required parameters are invalid.",
|
|
112
|
+
safeResolution: "Provide valid check ID, requirement, and parameters.",
|
|
113
|
+
}),
|
|
114
|
+
E_RECEIPT_STATE_MISMATCH: Object.freeze({
|
|
115
|
+
code: "E_RECEIPT_STATE_MISMATCH",
|
|
116
|
+
category: "verification",
|
|
117
|
+
classification: "PUBLIC_STABLE",
|
|
118
|
+
meaning: "Receipt does not match current state cycle or work state.",
|
|
119
|
+
safeResolution: "Run forgeloop prepare-completion --json.",
|
|
120
|
+
}),
|
|
121
|
+
E_CONTINUITY_RECONCILIATION_REQUIRED: Object.freeze({
|
|
122
|
+
code: "E_CONTINUITY_RECONCILIATION_REQUIRED",
|
|
123
|
+
category: "continuity",
|
|
124
|
+
classification: "PUBLIC_STABLE",
|
|
125
|
+
meaning: "Continuity context has drifted from work state.",
|
|
126
|
+
safeResolution: "Run forgeloop reconcile-continuity --json.",
|
|
127
|
+
}),
|
|
128
|
+
E_TASK_AMBIGUOUS: Object.freeze({
|
|
129
|
+
code: "E_TASK_AMBIGUOUS",
|
|
130
|
+
category: "task-resolution",
|
|
131
|
+
classification: "PUBLIC_STABLE",
|
|
132
|
+
meaning: "Multiple tasks exist in the project but no task selector was provided.",
|
|
133
|
+
safeResolution: "Select a task explicitly using --task <id> or FORGELOOP_TASK=<id>.",
|
|
134
|
+
}),
|
|
135
|
+
E_TASK_LOCKED: Object.freeze({
|
|
136
|
+
code: "E_TASK_LOCKED",
|
|
137
|
+
category: "concurrency",
|
|
138
|
+
classification: "PUBLIC_STABLE",
|
|
139
|
+
meaning: "Task mutation is currently locked by another concurrent process or run-check.",
|
|
140
|
+
safeResolution: "Wait for the active mutation to complete or inspect the lock with forgeloop task-show.",
|
|
141
|
+
}),
|
|
142
|
+
E_TASK_SCOPE_CONFLICT: Object.freeze({
|
|
143
|
+
code: "E_TASK_SCOPE_CONFLICT",
|
|
144
|
+
category: "scope",
|
|
145
|
+
classification: "PUBLIC_STABLE",
|
|
146
|
+
meaning: "Task write claims overlap with another non-complete task in the same checkout.",
|
|
147
|
+
safeResolution: "Adjust write claims to non-overlapping paths or run tasks in separate worktrees.",
|
|
148
|
+
}),
|
|
149
|
+
E_TASK_SCOPE_DIRTY: Object.freeze({
|
|
150
|
+
code: "E_TASK_SCOPE_DIRTY",
|
|
151
|
+
category: "scope",
|
|
152
|
+
classification: "PUBLIC_STABLE",
|
|
153
|
+
meaning: "Claimed paths contain pre-existing uncommitted changes.",
|
|
154
|
+
safeResolution: "Commit or stash changes in claimed paths before defining or adopting the scope.",
|
|
155
|
+
}),
|
|
156
|
+
E_TASK_CHANGE_OUTSIDE_SCOPE: Object.freeze({
|
|
157
|
+
code: "E_TASK_CHANGE_OUTSIDE_SCOPE",
|
|
158
|
+
category: "scope",
|
|
159
|
+
classification: "PUBLIC_STABLE",
|
|
160
|
+
meaning: "Modified paths in repository exceed the declared task write claims.",
|
|
161
|
+
safeResolution: "Update write claims with forgeloop task-scope or revert out-of-scope modifications.",
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
|
|
166
|
+
...FAILURE_CODES,
|
|
167
|
+
E_VERIFICATION_TOOL_UNAVAILABLE,
|
|
168
|
+
E_INSTALLATION_AUTHORITY_REQUIRED,
|
|
169
|
+
E_COMMAND_RESOLUTION_AMBIGUOUS,
|
|
170
|
+
E_AUTHORITY_INVALID,
|
|
171
|
+
E_AUTHORITY_SCOPE_MISMATCH,
|
|
172
|
+
"E_AUTHORITY_UNTRUSTED_SOURCE",
|
|
173
|
+
"E_EXECUTION_REF_INVALID",
|
|
174
|
+
"E_NATIVE_ADAPTER_STALE",
|
|
175
|
+
"E_NATIVE_ADAPTER_TARGET_MISSING",
|
|
176
|
+
"E_MIGRATION_INCOMPLETE",
|
|
177
|
+
"E_MIGRATION_WRITE_VERIFY",
|
|
178
|
+
E_TASK_REQUIRED,
|
|
179
|
+
E_TASK_NOT_FOUND,
|
|
180
|
+
E_TASK_ALREADY_EXISTS,
|
|
181
|
+
E_TASK_AMBIGUOUS,
|
|
182
|
+
E_TASK_SELECTOR_CONFLICT,
|
|
183
|
+
E_TASK_DESCRIPTOR_INVALID,
|
|
184
|
+
E_TASK_KEY_MISMATCH,
|
|
185
|
+
E_TASK_CONTEXT_MISMATCH,
|
|
186
|
+
E_TASK_LOCKED,
|
|
187
|
+
E_TASK_LOCK_INVALID,
|
|
188
|
+
E_TASK_SCOPE_REQUIRED,
|
|
189
|
+
E_TASK_SCOPE_CONFLICT,
|
|
190
|
+
E_TASK_SCOPE_DIRTY,
|
|
191
|
+
E_TASK_SCOPE_FROZEN,
|
|
192
|
+
E_TASK_CHANGE_OUTSIDE_SCOPE,
|
|
193
|
+
E_TASK_CHANGE_ATTRIBUTION_UNAVAILABLE,
|
|
194
|
+
E_TASK_LAYOUT_LEGACY,
|
|
195
|
+
E_TASK_MIGRATION_INVALID,
|
|
196
|
+
E_TASK_MIGRATION_IDENTITY_MISMATCH,
|
|
197
|
+
]));
|
package/src/core/events.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
@@ -10,6 +10,8 @@ import { assertSecretFree } from "./receipt.js";
|
|
|
10
10
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
11
11
|
import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
|
|
12
12
|
|
|
13
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
14
|
+
|
|
13
15
|
const EVENT_SCHEMA_VERSION = 1;
|
|
14
16
|
export const LIFECYCLE_MILESTONES = Object.freeze([
|
|
15
17
|
"CONTRACT_VALIDATED",
|
|
@@ -47,22 +49,23 @@ function protocolError(code, message, artifacts = [ARTIFACT_PATHS.events]) {
|
|
|
47
49
|
return error;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
async function readEvents(target, packageRoot) {
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
export async function readEvents(target, packageRoot, options = {}) {
|
|
53
|
+
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
54
|
+
await assertSafePath(target, relPath);
|
|
55
|
+
const eventsPath = ensureWithin(target, relPath);
|
|
53
56
|
if (!(await fileExists(eventsPath))) return [];
|
|
54
57
|
const text = await readFile(eventsPath, "utf8");
|
|
55
|
-
assertJsonBytes(text,
|
|
58
|
+
assertJsonBytes(text, relPath);
|
|
56
59
|
const schema = await readSchema("event", packageRoot);
|
|
57
60
|
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
58
61
|
return lines.map((line, index) => {
|
|
59
62
|
let event;
|
|
60
63
|
try {
|
|
61
64
|
event = JSON.parse(line);
|
|
62
|
-
assertJsonLimits(event, `${
|
|
63
|
-
assertSchema(event, schema, `${
|
|
65
|
+
assertJsonLimits(event, `${relPath}[${index}]`);
|
|
66
|
+
assertSchema(event, schema, `${relPath}[${index}]`);
|
|
64
67
|
} catch (error) {
|
|
65
|
-
throw protocolError("E_EVENT_INVALID", `${
|
|
68
|
+
throw protocolError("E_EVENT_INVALID", `${relPath} line ${index + 1}: ${error.message}`, [relPath]);
|
|
66
69
|
}
|
|
67
70
|
return event;
|
|
68
71
|
});
|
|
@@ -71,7 +74,8 @@ async function readEvents(target, packageRoot) {
|
|
|
71
74
|
export async function appendProtocolEvent(target, input, packageRoot, options = {}) {
|
|
72
75
|
if (typeof input?.taskId !== "string" || !input.taskId) throw protocolError("E_EVENT_INVALID", "event taskId is required");
|
|
73
76
|
if (typeof input?.event !== "string" || !input.event) throw protocolError("E_EVENT_INVALID", "event type is required");
|
|
74
|
-
const
|
|
77
|
+
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
78
|
+
const events = await readEvents(target, packageRoot, { ...options, eventsPath: relPath });
|
|
75
79
|
const previous = events.at(-1) ?? null;
|
|
76
80
|
const event = {
|
|
77
81
|
seq: events.length + 1,
|
|
@@ -86,9 +90,9 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
|
|
|
86
90
|
};
|
|
87
91
|
assertSecretFree(event);
|
|
88
92
|
const schema = await readSchema("event", packageRoot);
|
|
89
|
-
assertSchema(event, schema,
|
|
93
|
+
assertSchema(event, schema, relPath);
|
|
90
94
|
event.hash = eventHash(event);
|
|
91
|
-
const eventsPath = ensureWithin(target,
|
|
95
|
+
const eventsPath = ensureWithin(target, relPath);
|
|
92
96
|
if (!options.dryRun) {
|
|
93
97
|
await mkdir(path.dirname(eventsPath), { recursive: true });
|
|
94
98
|
await appendFile(eventsPath, `${JSON.stringify(event)}\n`, { encoding: "utf8" });
|
|
@@ -96,12 +100,13 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
|
|
|
96
100
|
return event;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
export async function validateEventLedger(target, packageRoot) {
|
|
103
|
+
export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
104
|
+
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
100
105
|
let events;
|
|
101
106
|
try {
|
|
102
|
-
events = await readEvents(target, packageRoot);
|
|
107
|
+
events = await readEvents(target, packageRoot, { ...options, eventsPath: relPath });
|
|
103
108
|
} catch (error) {
|
|
104
|
-
return { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message }] };
|
|
109
|
+
return { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message, artifacts: [relPath] }] };
|
|
105
110
|
}
|
|
106
111
|
const errors = [];
|
|
107
112
|
let taskId = null;
|
package/src/core/execution.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileExists } from "./filesystem.js";
|
|
4
6
|
import {
|
|
5
7
|
ARTIFACT_PATHS,
|
|
6
8
|
executionArtifactPath,
|
|
7
9
|
readJsonArtifact,
|
|
8
10
|
writeJsonArtifact,
|
|
9
11
|
} from "./artifacts.js";
|
|
12
|
+
import { taskExecutionPath } from "./task-paths.js";
|
|
10
13
|
import {
|
|
11
|
-
classifyCommandResolution,
|
|
12
14
|
resolveExecutionResolution,
|
|
13
15
|
validateVerificationAuthority,
|
|
14
16
|
E_COMMAND_RESOLUTION_AMBIGUOUS,
|
|
@@ -86,6 +88,7 @@ export async function runCommandExecution({
|
|
|
86
88
|
details,
|
|
87
89
|
authorityContext,
|
|
88
90
|
runtimeContext,
|
|
91
|
+
executionPath,
|
|
89
92
|
} = {}) {
|
|
90
93
|
const commandArgv = normalizeArgv(argv);
|
|
91
94
|
const resolution = await resolveExecutionResolution({
|
|
@@ -150,8 +153,8 @@ export async function runCommandExecution({
|
|
|
150
153
|
status: processResult.exitCode === 0 && !processResult.spawnError ? "passed" : "failed",
|
|
151
154
|
exitCode: processResult.exitCode,
|
|
152
155
|
};
|
|
153
|
-
const
|
|
154
|
-
const written = await writeJsonArtifact(target,
|
|
156
|
+
const execPath = executionPath ?? (taskId ? taskExecutionPath(taskId, executionId) : executionArtifactPath(executionId));
|
|
157
|
+
const written = await writeJsonArtifact(target, execPath, execution, "execution", packageRoot);
|
|
155
158
|
return {
|
|
156
159
|
path: written.path,
|
|
157
160
|
execution: written.value,
|
|
@@ -161,14 +164,43 @@ export async function runCommandExecution({
|
|
|
161
164
|
};
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
export async function readExecutionArtifact({ target, executionRef, packageRoot } = {}) {
|
|
167
|
+
export async function readExecutionArtifact({ target, executionRef, packageRoot, taskId } = {}) {
|
|
165
168
|
let relativePath;
|
|
166
169
|
try {
|
|
167
|
-
relativePath = executionArtifactPath(executionRef);
|
|
170
|
+
relativePath = taskId ? taskExecutionPath(taskId, executionRef) : executionArtifactPath(executionRef);
|
|
168
171
|
const artifact = await readJsonArtifact(target, relativePath, "execution", packageRoot);
|
|
169
172
|
return artifact;
|
|
170
173
|
} catch (error) {
|
|
171
174
|
if (error.code === "E_EXECUTION_REF_INVALID") throw error;
|
|
175
|
+
if (error.code === "ARTIFACT_MISSING") {
|
|
176
|
+
if (taskId) {
|
|
177
|
+
try {
|
|
178
|
+
const fallbackPath = executionArtifactPath(executionRef);
|
|
179
|
+
const artifact = await readJsonArtifact(target, fallbackPath, "execution", packageRoot);
|
|
180
|
+
return artifact;
|
|
181
|
+
} catch {
|
|
182
|
+
// ignore fallback failure
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
try {
|
|
186
|
+
const taskStateDir = path.join(target, ".forgeloop", "task-state");
|
|
187
|
+
if (await fileExists(taskStateDir)) {
|
|
188
|
+
const entries = await readdir(taskStateDir, { withFileTypes: true });
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
const execFile = path.join(taskStateDir, entry.name, "executions", `${executionRef}.json`);
|
|
192
|
+
if (await fileExists(execFile)) {
|
|
193
|
+
const rel = path.relative(target, execFile).replaceAll("\\", "/");
|
|
194
|
+
return await readJsonArtifact(target, rel, "execution", packageRoot);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// ignore scan failure
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
172
204
|
throw executionError("E_EXECUTION_REF_INVALID", "Execution reference does not resolve to a valid ForgeLoop artifact", [relativePath ?? ARTIFACT_PATHS.executionDirectory]);
|
|
173
205
|
}
|
|
174
206
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
1
|
import { assertSafePath, ensureWithin, fileExists, readBytes } from "./filesystem.js";
|
|
4
2
|
import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
5
3
|
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
6
4
|
import { sha256 } from "./manifest.js";
|
|
5
|
+
import { taskGatePath } from "./task-paths.js";
|
|
7
6
|
|
|
8
7
|
function gateName(value) {
|
|
9
8
|
if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(value)) {
|
|
@@ -14,22 +13,26 @@ function gateName(value) {
|
|
|
14
13
|
return value;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
export function gatePath(gate) {
|
|
18
|
-
|
|
16
|
+
export function gatePath(gate, options = {}) {
|
|
17
|
+
const name = gateName(gate);
|
|
18
|
+
if (options?.taskId) {
|
|
19
|
+
return taskGatePath(options.taskId, name);
|
|
20
|
+
}
|
|
21
|
+
return `${ARTIFACT_PATHS.gates ?? ".forgeloop/gates"}/${name}.json`;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export async function persistGate(target, gate, packageRoot, options = {}) {
|
|
22
|
-
const relativePath = gatePath(gate.gate);
|
|
25
|
+
const relativePath = options?.gatePath ?? (options?.taskId ? taskGatePath(options.taskId, gate.gate) : gatePath(gate.gate, options));
|
|
23
26
|
return writeJsonArtifact(target, relativePath, gate, "gate", packageRoot, options);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
export async function readGate(target, gate, packageRoot) {
|
|
27
|
-
const relativePath = gatePath(gate);
|
|
29
|
+
export async function readGate(target, gate, packageRoot, options = {}) {
|
|
30
|
+
const relativePath = options?.gatePath ?? (options?.taskId ? taskGatePath(options.taskId, gate) : gatePath(gate, options));
|
|
28
31
|
return readJsonArtifact(target, relativePath, "gate", packageRoot);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
export async function readGateIfPresent(target, gate, packageRoot) {
|
|
32
|
-
const relativePath = gatePath(gate);
|
|
34
|
+
export async function readGateIfPresent(target, gate, packageRoot, options = {}) {
|
|
35
|
+
const relativePath = options?.gatePath ?? (options?.taskId ? taskGatePath(options.taskId, gate) : gatePath(gate, options));
|
|
33
36
|
await assertSafePath(target, relativePath);
|
|
34
37
|
const filePath = ensureWithin(target, relativePath);
|
|
35
38
|
if (!(await fileExists(filePath))) return null;
|
package/src/core/gates.js
CHANGED
|
@@ -50,6 +50,8 @@ export function createGate(input = {}) {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
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`));
|
|
55
57
|
}
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
premium: "ENG/premium-sites-studio-eng.md",
|
|
6
|
-
taste: "ENG/taste-frontend-eng.md",
|
|
7
|
-
clean: "ENG/clean-code-eng.md",
|
|
8
|
-
test: "ENG/test-code-eng.md",
|
|
9
|
-
security: "ENG/sec-code-eng.md",
|
|
10
|
-
design: "ENG/design-code-eng.md",
|
|
11
|
-
performance: "ENG/perf-code-eng.md",
|
|
12
|
-
accessibility: "ENG/accessibility-eng.md",
|
|
13
|
-
games: "ENG/games-code-design-web-eng.md",
|
|
14
|
-
});
|
|
4
|
+
import { GUIDE_FILES } from "./guide-registry.js";
|
|
15
5
|
|
|
16
6
|
function parseList(frontmatter, key) {
|
|
17
7
|
const lines = frontmatter.split(/\r?\n/);
|
|
@@ -30,8 +20,12 @@ function parseFrontmatter(text, relativePath) {
|
|
|
30
20
|
if (!match) throw new Error(`${relativePath} is missing YAML frontmatter`);
|
|
31
21
|
const frontmatter = match[1];
|
|
32
22
|
const guideId = frontmatter.match(/^guide-id:\s*([^\s]+)\s*$/m)?.[1] ?? null;
|
|
23
|
+
const lastReviewed = frontmatter.match(/^last-reviewed:\s*"?([^"\r\n]+)"?\s*$/m)?.[1] ?? null;
|
|
24
|
+
const version = frontmatter.match(/^version:\s*"?([^"\r\n]+)"?\s*$/m)?.[1] ?? null;
|
|
33
25
|
return {
|
|
34
26
|
guideId,
|
|
27
|
+
lastReviewed,
|
|
28
|
+
version,
|
|
35
29
|
requiresGates: parseList(frontmatter, "requires-gates"),
|
|
36
30
|
completionEvidence: parseList(frontmatter, "completion-evidence"),
|
|
37
31
|
};
|
|
@@ -44,6 +38,8 @@ export async function readGuideMetadata(packageRoot) {
|
|
|
44
38
|
const parsed = parseFrontmatter(text, relativePath);
|
|
45
39
|
metadata[id] = {
|
|
46
40
|
guideId: parsed.guideId ?? id,
|
|
41
|
+
lastReviewed: parsed.lastReviewed,
|
|
42
|
+
version: parsed.version,
|
|
47
43
|
requiresGates: [...parsed.requiresGates],
|
|
48
44
|
completionEvidence: [...parsed.completionEvidence],
|
|
49
45
|
path: relativePath,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const rawGuides = JSON.parse(
|
|
7
|
+
readFileSync(path.resolve(currentDir, "../config/guides.json"), "utf8"),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
export const GUIDE_REGISTRY = Object.freeze(
|
|
11
|
+
Object.fromEntries(
|
|
12
|
+
Object.entries(rawGuides).map(([id, def]) => [
|
|
13
|
+
id,
|
|
14
|
+
Object.freeze({ id, ...def }),
|
|
15
|
+
]),
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const GUIDE_IDS = Object.freeze(Object.keys(GUIDE_REGISTRY));
|
|
20
|
+
|
|
21
|
+
export const GUIDE_FILES = Object.freeze(
|
|
22
|
+
Object.fromEntries(Object.entries(GUIDE_REGISTRY).map(([id, def]) => [id, def.path])),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const GUIDE_TEMPLATE_PATHS = Object.freeze(
|
|
26
|
+
Object.values(GUIDE_REGISTRY)
|
|
27
|
+
.filter((def) => def.install)
|
|
28
|
+
.map((def) => def.path),
|
|
29
|
+
);
|
package/src/core/inspect.js
CHANGED
|
@@ -20,7 +20,9 @@ function profileMetadata(bytes) {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
24
|
+
|
|
25
|
+
export async function inspectTarget({ target, packageRoot, contractFile = null, authorityContext, runtimeContext, taskId = null, stateFile = null } = {}) {
|
|
24
26
|
let manifest = null;
|
|
25
27
|
let manifestError = null;
|
|
26
28
|
try {
|
|
@@ -34,10 +36,11 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
34
36
|
const profile = (profilePath && await fileExists(profilePath))
|
|
35
37
|
? { ...profileMetadata(await readBytes(profilePath)), path: profileRelativePath }
|
|
36
38
|
: { mode: null, status: null };
|
|
37
|
-
const
|
|
39
|
+
const effectiveStateRel = stateFile ?? (taskId ? taskArtifactPath(taskId, "state") : WORK_STATE_PATH);
|
|
40
|
+
const statePath = ensureWithin(target, effectiveStateRel);
|
|
38
41
|
const statePresent = await fileExists(statePath);
|
|
39
|
-
const state = await readAndClassifyWorkState({ target, packageRoot, contractFile });
|
|
40
|
-
const continuity = await reconcileContinuity({ target, packageRoot });
|
|
42
|
+
const state = await readAndClassifyWorkState({ target, packageRoot, contractFile, taskId, stateFile: effectiveStateRel });
|
|
43
|
+
const continuity = await reconcileContinuity({ target, packageRoot, taskId });
|
|
41
44
|
const schemaRoot = manifest?.layoutVersion >= 2
|
|
42
45
|
? ensureWithin(target, FORGELOOP_KIT_DIR)
|
|
43
46
|
: target;
|
|
@@ -40,6 +40,12 @@ Read and follow:
|
|
|
40
40
|
Before changing product or executable files, establish the ForgeLoop contract,
|
|
41
41
|
route, required gates, and READY preflight.
|
|
42
42
|
|
|
43
|
+
Before creating or activating new lifecycle state:
|
|
44
|
+
If \`.forgeloop/work-state.json\` exists, inspect the existing task,
|
|
45
|
+
reconcile continuity when present, inspect the checkout, and run
|
|
46
|
+
\`forgeloop next\`. A change of harness, model, provider, IDE, process,
|
|
47
|
+
terminal, or session does not create a new task.
|
|
48
|
+
|
|
43
49
|
Use the project-local ForgeLoop CLI for lifecycle-owned protocol state.
|
|
44
50
|
Never manually synthesize lifecycle chronology or assign ForgeLoop COMPLETE.
|
|
45
51
|
|