@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.
Files changed (95) hide show
  1. package/.cursor/rules/project-loop.mdc +3 -2
  2. package/.github/copilot-instructions.md +1 -0
  3. package/AGENTS.md +2 -1
  4. package/CLAUDE.md +1 -0
  5. package/DOCS_INDEX.md +36 -0
  6. package/ENG/design-code-eng.md +31 -0
  7. package/ENG/documentation-quality-eng.md +400 -0
  8. package/EXECUTION_STATE.md +23 -13
  9. package/GUIDE_ROUTER.md +23 -8
  10. package/LOOP_ENGINEERING.md +77 -12
  11. package/PROTOCOL_INTEGRATION.md +9 -6
  12. package/README.md +82 -39
  13. package/docs/ARTIFACT_REFERENCE.md +442 -0
  14. package/docs/CLI_REFERENCE.md +892 -0
  15. package/docs/CROSS_HARNESS_CONTINUITY.md +198 -0
  16. package/docs/DOCUMENTATION_GUIDE.md +161 -0
  17. package/docs/GETTING_STARTED.md +348 -0
  18. package/docs/RECIPES.md +250 -0
  19. package/docs/TROUBLESHOOTING.md +345 -0
  20. package/docs/assets/forgeloop-flow.svg +1 -1
  21. package/docs/forgeloop-flow.mmd +1 -1
  22. package/package.json +13 -2
  23. package/schemas/task-descriptor.schema.json +56 -0
  24. package/schemas/work-state.schema.json +18 -1
  25. package/scripts/CI_VALIDATORS.md +7 -0
  26. package/src/cli.js +280 -388
  27. package/src/commands/advance.js +5 -2
  28. package/src/commands/audit.js +11 -1
  29. package/src/commands/clear-continuity.js +5 -2
  30. package/src/commands/clear-state.js +5 -2
  31. package/src/commands/complete.js +9 -1
  32. package/src/commands/continuity.js +5 -2
  33. package/src/commands/inspect.js +10 -2
  34. package/src/commands/next.js +5 -2
  35. package/src/commands/preflight.js +9 -1
  36. package/src/commands/prepare-completion.js +5 -2
  37. package/src/commands/reconcile-continuity.js +5 -2
  38. package/src/commands/record-check.js +7 -1
  39. package/src/commands/record-continuity.js +21 -14
  40. package/src/commands/record-terminal-result.js +7 -1
  41. package/src/commands/route.js +22 -18
  42. package/src/commands/run-check.js +52 -44
  43. package/src/commands/status.js +18 -12
  44. package/src/commands/task-create.js +94 -0
  45. package/src/commands/task-list.js +48 -0
  46. package/src/commands/task-migrate.js +34 -0
  47. package/src/commands/task-scope.js +75 -0
  48. package/src/commands/task-show.js +81 -0
  49. package/src/commands/task-unlock.js +35 -0
  50. package/src/commands/validate-protocol.js +37 -20
  51. package/src/commands/validate-state.js +24 -18
  52. package/src/config/guides.json +42 -0
  53. package/src/core/activation.js +8 -4
  54. package/src/core/artifact-registry.js +166 -0
  55. package/src/core/audit.js +65 -12
  56. package/src/core/bundles.js +76 -50
  57. package/src/core/cli-command-definitions.js +611 -0
  58. package/src/core/cli-metadata.js +23 -0
  59. package/src/core/completion-artifacts.js +161 -74
  60. package/src/core/completion.js +134 -76
  61. package/src/core/continuity.js +20 -13
  62. package/src/core/contract.js +6 -3
  63. package/src/core/error-codes.js +197 -0
  64. package/src/core/events.js +19 -14
  65. package/src/core/execution.js +38 -6
  66. package/src/core/gate-artifact.js +12 -9
  67. package/src/core/gates.js +4 -2
  68. package/src/core/guide-metadata.js +7 -11
  69. package/src/core/guide-registry.js +29 -0
  70. package/src/core/inspect.js +7 -4
  71. package/src/core/native-adapters.js +6 -0
  72. package/src/core/phase.js +85 -33
  73. package/src/core/preflight-consistency.js +24 -14
  74. package/src/core/preflight-loaders.js +16 -11
  75. package/src/core/preflight.js +44 -25
  76. package/src/core/protocol.js +2 -11
  77. package/src/core/receipt.js +1 -1
  78. package/src/core/report.js +2 -2
  79. package/src/core/repository.js +46 -12
  80. package/src/core/resumability.js +6 -4
  81. package/src/core/route-artifact.js +9 -5
  82. package/src/core/router.js +11 -7
  83. package/src/core/schema-validation.js +1 -0
  84. package/src/core/task-command.js +41 -0
  85. package/src/core/task-context.js +126 -0
  86. package/src/core/task-descriptor.js +81 -0
  87. package/src/core/task-discovery.js +116 -0
  88. package/src/core/task-identity.js +76 -0
  89. package/src/core/task-lock.js +209 -0
  90. package/src/core/task-migration-validation.js +140 -0
  91. package/src/core/task-migration.js +361 -0
  92. package/src/core/task-paths.js +96 -0
  93. package/src/core/task-scope.js +179 -0
  94. package/src/core/templates.js +3 -9
  95. package/src/core/work-state.js +24 -13
@@ -0,0 +1,96 @@
1
+ import { assertTaskId, taskStorageKey } from "./task-identity.js";
2
+
3
+ export const TASK_STATE_ROOT = ".forgeloop/task-state";
4
+ export const SESSIONS_ROOT = ".forgeloop/sessions";
5
+
6
+ export const TASK_ARTIFACT_FILES = Object.freeze({
7
+ descriptor: "task.json",
8
+ contract: "contract.json",
9
+ route: "routing-result.json",
10
+ preflight: "preflight.json",
11
+ state: "work-state.json",
12
+ continuity: "continuity.json",
13
+ receipt: "execution-receipt.json",
14
+ events: "events.ndjson",
15
+ gates: "gates",
16
+ executions: "executions",
17
+ lock: ".lock",
18
+ });
19
+
20
+ export const PROJECT_ARTIFACT_PATHS = Object.freeze({
21
+ config: ".forgeloop/config.json",
22
+ sources: ".forgeloop/sources.json",
23
+ manifest: ".forgeloop/.manifest.json",
24
+ kit: ".forgeloop/kit",
25
+ gitignore: ".forgeloop/.gitignore",
26
+ });
27
+
28
+ export const LEGACY_TASK_ARTIFACT_PATHS = Object.freeze({
29
+ contract: ".forgeloop/current-contract.json",
30
+ route: ".forgeloop/routing-result.json",
31
+ preflight: ".forgeloop/preflight.json",
32
+ state: ".forgeloop/work-state.json",
33
+ continuity: ".forgeloop/continuity.json",
34
+ receipt: ".forgeloop/execution-receipt.json",
35
+ events: ".forgeloop/events.ndjson",
36
+ gates: ".forgeloop/gates",
37
+ executions: ".forgeloop/executions",
38
+ session: ".forgeloop/session.json",
39
+ });
40
+
41
+ export function taskDirectory(taskId) {
42
+ return `${TASK_STATE_ROOT}/${taskStorageKey(taskId)}`;
43
+ }
44
+
45
+ export function taskArtifactPath(taskId, key) {
46
+ const relative = TASK_ARTIFACT_FILES[key];
47
+ if (!relative) {
48
+ throw new Error(`Unknown task artifact: ${key}`);
49
+ }
50
+ return `${taskDirectory(taskId)}/${relative}`;
51
+ }
52
+
53
+ export function taskGatePath(taskId, gateId) {
54
+ if (typeof gateId !== "string" || !gateId || gateId.includes("/") || gateId.includes("\\") || gateId.includes("..")) {
55
+ throw new Error(`Invalid gate ID: ${gateId}`);
56
+ }
57
+ const cleanId = gateId.endsWith(".json") ? gateId : `${gateId}.json`;
58
+ return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.gates}/${cleanId}`;
59
+ }
60
+
61
+ export function taskExecutionPath(taskId, executionId) {
62
+ if (typeof executionId !== "string" || !/^exec-[A-Za-z0-9_-]+$/.test(executionId)) {
63
+ throw new Error(`Invalid execution ID: ${executionId}`);
64
+ }
65
+ return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.executions}/${executionId}.json`;
66
+ }
67
+
68
+ export function taskLockPath(taskId) {
69
+ return taskArtifactPath(taskId, "lock");
70
+ }
71
+
72
+ export function sessionArtifactPath(sessionId) {
73
+ if (typeof sessionId !== "string" || !sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
74
+ throw new Error(`Invalid session ID: ${sessionId}`);
75
+ }
76
+ const cleanId = sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`;
77
+ return `${SESSIONS_ROOT}/${cleanId}`;
78
+ }
79
+
80
+ export function buildTaskArtifactPaths(taskId) {
81
+ assertTaskId(taskId);
82
+ const dir = taskDirectory(taskId);
83
+ return Object.freeze({
84
+ descriptor: `${dir}/${TASK_ARTIFACT_FILES.descriptor}`,
85
+ contract: `${dir}/${TASK_ARTIFACT_FILES.contract}`,
86
+ route: `${dir}/${TASK_ARTIFACT_FILES.route}`,
87
+ preflight: `${dir}/${TASK_ARTIFACT_FILES.preflight}`,
88
+ state: `${dir}/${TASK_ARTIFACT_FILES.state}`,
89
+ continuity: `${dir}/${TASK_ARTIFACT_FILES.continuity}`,
90
+ receipt: `${dir}/${TASK_ARTIFACT_FILES.receipt}`,
91
+ events: `${dir}/${TASK_ARTIFACT_FILES.events}`,
92
+ gates: `${dir}/${TASK_ARTIFACT_FILES.gates}`,
93
+ executions: `${dir}/${TASK_ARTIFACT_FILES.executions}`,
94
+ lock: `${dir}/${TASK_ARTIFACT_FILES.lock}`,
95
+ });
96
+ }
@@ -0,0 +1,179 @@
1
+ import path from "node:path";
2
+ import {
3
+ E_TASK_CHANGE_OUTSIDE_SCOPE,
4
+ E_TASK_DESCRIPTOR_INVALID,
5
+ E_TASK_SCOPE_CONFLICT,
6
+ E_TASK_SCOPE_DIRTY,
7
+ E_TASK_SCOPE_FROZEN,
8
+ } from "./error-codes.js";
9
+ import { currentChangedPaths } from "./repository.js";
10
+
11
+ const FROZEN_PHASES = new Set([
12
+ "EXECUTING",
13
+ "VERIFYING",
14
+ "DIAGNOSING",
15
+ "CORRECTING",
16
+ "REVIEWING",
17
+ "COMPLETE",
18
+ ]);
19
+
20
+ export function normalizeWriteClaim(claim) {
21
+ if (typeof claim !== "string" || claim.trim() === "") {
22
+ const error = new Error("Write claim must be a non-empty string");
23
+ error.code = E_TASK_DESCRIPTOR_INVALID;
24
+ throw error;
25
+ }
26
+
27
+ const portable = claim.trim().replaceAll("\\", "/");
28
+ if (portable.startsWith("/") || /^[A-Za-z]:\//.test(portable)) {
29
+ const error = new Error(`Write claim must remain relative: ${claim}`);
30
+ error.code = E_TASK_DESCRIPTOR_INVALID;
31
+ throw error;
32
+ }
33
+
34
+ const normalized = path.posix.normalize(portable);
35
+ if (normalized === ".." || normalized.startsWith("../")) {
36
+ const error = new Error(`Write claim escapes project directory: ${claim}`);
37
+ error.code = E_TASK_DESCRIPTOR_INVALID;
38
+ throw error;
39
+ }
40
+
41
+ if (normalized === "." || normalized === "./") {
42
+ return ".";
43
+ }
44
+
45
+ return normalized.replace(/^\.\//, "").replace(/\/+$/, "");
46
+ }
47
+
48
+ export function normalizeWriteClaims(claims) {
49
+ if (claims === undefined || claims === null) {
50
+ return [];
51
+ }
52
+ if (!Array.isArray(claims)) {
53
+ const error = new Error("writeClaims must be an array of string prefixes");
54
+ error.code = E_TASK_DESCRIPTOR_INVALID;
55
+ throw error;
56
+ }
57
+
58
+ const normalized = claims.map((c) => normalizeWriteClaim(c));
59
+ const unique = [...new Set(normalized)];
60
+ return unique.sort((a, b) => a.localeCompare(b));
61
+ }
62
+
63
+ export function claimsOverlap(claimA, claimB) {
64
+ const normA = normalizeWriteClaim(claimA).toLowerCase();
65
+ const normB = normalizeWriteClaim(claimB).toLowerCase();
66
+
67
+ if (normA === "." || normB === ".") {
68
+ return true;
69
+ }
70
+
71
+ if (normA === normB) {
72
+ return true;
73
+ }
74
+
75
+ if (normA.startsWith(`${normB}/`)) {
76
+ return true;
77
+ }
78
+
79
+ if (normB.startsWith(`${normA}/`)) {
80
+ return true;
81
+ }
82
+
83
+ return false;
84
+ }
85
+
86
+ export function checkScopeConflicts(newClaims, existingTasks = [], currentTaskId = null) {
87
+ const normalizedNew = normalizeWriteClaims(newClaims);
88
+ if (normalizedNew.length === 0) return [];
89
+
90
+ const conflicts = [];
91
+ for (const task of existingTasks) {
92
+ if (task.taskId === currentTaskId) continue;
93
+ // Only non-COMPLETE tasks hold active write claims
94
+ if (task.phase === "COMPLETE") continue;
95
+
96
+ const taskClaims = normalizeWriteClaims(task.writeClaims ?? task.descriptor?.writeClaims ?? []);
97
+ for (const newClaim of normalizedNew) {
98
+ for (const existingClaim of taskClaims) {
99
+ if (claimsOverlap(newClaim, existingClaim)) {
100
+ conflicts.push({
101
+ taskId: task.taskId,
102
+ phase: task.phase,
103
+ conflictingClaim: existingClaim,
104
+ requestedClaim: newClaim,
105
+ });
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ return conflicts;
112
+ }
113
+
114
+ export function assertNoScopeConflicts(newClaims, existingTasks = [], currentTaskId = null) {
115
+ const conflicts = checkScopeConflicts(newClaims, existingTasks, currentTaskId);
116
+ if (conflicts.length > 0) {
117
+ const first = conflicts[0];
118
+ const error = new Error(
119
+ `Task write claim "${first.requestedClaim}" conflicts with active task "${first.taskId}" (claim "${first.conflictingClaim}", phase: ${first.phase ?? "PLANNED"})`,
120
+ );
121
+ error.code = E_TASK_SCOPE_CONFLICT;
122
+ error.conflicts = conflicts;
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ export async function assertScopeClean(target, claims) {
128
+ const normalized = normalizeWriteClaims(claims);
129
+ if (normalized.length === 0) return;
130
+
131
+ const changed = await currentChangedPaths(target, { paths: normalized });
132
+ if (changed !== null && changed.length > 0) {
133
+ const error = new Error(
134
+ `Claimed scope contains pre-existing uncommitted changes: ${changed.slice(0, 5).join(", ")}${changed.length > 5 ? "..." : ""}`,
135
+ );
136
+ error.code = E_TASK_SCOPE_DIRTY;
137
+ error.changedPaths = changed;
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ export function assertScopeNotFrozen(phase) {
143
+ if (phase && FROZEN_PHASES.has(phase)) {
144
+ const error = new Error(
145
+ `Task scope cannot be modified once execution lifecycle has started (current phase: ${phase})`,
146
+ );
147
+ error.code = E_TASK_SCOPE_FROZEN;
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ export function assertClaimsCoverChangedPaths(claims, changedPaths) {
153
+ const normalizedClaims = normalizeWriteClaims(claims);
154
+ if (normalizedClaims.length === 0) return;
155
+
156
+ // Root claim covers everything
157
+ if (normalizedClaims.includes(".")) return;
158
+
159
+ const outOfScope = [];
160
+ for (const changedPath of changedPaths ?? []) {
161
+ const normPath = changedPath.replaceAll("\\", "/").replace(/^\.\//, "").toLowerCase();
162
+ const covered = normalizedClaims.some((claim) => {
163
+ const normClaim = claim.toLowerCase();
164
+ return normPath === normClaim || normPath.startsWith(`${normClaim}/`);
165
+ });
166
+ if (!covered) {
167
+ outOfScope.push(changedPath);
168
+ }
169
+ }
170
+
171
+ if (outOfScope.length > 0) {
172
+ const error = new Error(
173
+ `Observed repository changes exceed task write claims: ${outOfScope.slice(0, 5).join(", ")}${outOfScope.length > 5 ? "..." : ""}`,
174
+ );
175
+ error.code = E_TASK_CHANGE_OUTSIDE_SCOPE;
176
+ error.outOfScopePaths = outOfScope;
177
+ throw error;
178
+ }
179
+ }
@@ -8,6 +8,7 @@ import {
8
8
  targetPathForSource,
9
9
  } from "./target-layout.js";
10
10
  import { nativeShim } from "./native-adapters.js";
11
+ import { GUIDE_TEMPLATE_PATHS } from "./guide-registry.js";
11
12
 
12
13
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
13
14
 
@@ -37,15 +38,7 @@ export const TEMPLATE_PATHS = [
37
38
  "THIRD_PARTY_NOTICES.md",
38
39
  "LICENSE",
39
40
  "LICENSE-DOCS.md",
40
- "ENG/accessibility-eng.md",
41
- "ENG/clean-code-eng.md",
42
- "ENG/design-code-eng.md",
43
- "ENG/games-code-design-web-eng.md",
44
- "ENG/perf-code-eng.md",
45
- "ENG/premium-sites-studio-eng.md",
46
- "ENG/taste-frontend-eng.md",
47
- "ENG/sec-code-eng.md",
48
- "ENG/test-code-eng.md",
41
+ ...GUIDE_TEMPLATE_PATHS,
49
42
  "schemas/routing-input.schema.json",
50
43
  "schemas/routing-result.schema.json",
51
44
  "schemas/work-state.schema.json",
@@ -67,6 +60,7 @@ export const TEMPLATE_PATHS = [
67
60
  "schemas/policy.schema.json",
68
61
  "schemas/task-bundle.schema.json",
69
62
  "schemas/authority.schema.json",
63
+ "schemas/task-descriptor.schema.json",
70
64
  ];
71
65
 
72
66
  export function getPackageRoot() {
@@ -18,6 +18,7 @@ import { createEvidence } from "./evidence.js";
18
18
  import { assertJsonBytes } from "./json-safety.js";
19
19
  import { assertCoverageList } from "./coverage.js";
20
20
  import { canonicalFingerprint } from "./artifacts.js";
21
+ import { taskArtifactPath } from "./task-paths.js";
21
22
 
22
23
  export const WORK_STATE_PATH = ".forgeloop/work-state.json";
23
24
 
@@ -229,17 +230,22 @@ async function validateStoredState(state, packageRoot = getPackageRoot()) {
229
230
  return assertWorkStateSemantics(state);
230
231
  }
231
232
 
232
- export async function readWorkState(target, packageRoot = getPackageRoot()) {
233
- await assertSafePath(target, WORK_STATE_PATH);
234
- const statePath = ensureWithin(target, WORK_STATE_PATH);
233
+ export async function readWorkState(target, options = {}) {
234
+ const packageRoot = typeof options === "string" ? options : (options?.packageRoot ?? getPackageRoot());
235
+ const relPath = typeof options === "object" && options !== null
236
+ ? (options.statePath ?? options.relativePath ?? (options.taskId ? taskArtifactPath(options.taskId, "state") : (options.taskContext ? options.taskContext.paths.state : WORK_STATE_PATH)))
237
+ : WORK_STATE_PATH;
238
+
239
+ await assertSafePath(target, relPath);
240
+ const statePath = ensureWithin(target, relPath);
235
241
  if (!(await fileExists(statePath))) return null;
236
242
  let state;
237
243
  try {
238
244
  const bytes = await readBytes(statePath);
239
- assertJsonBytes(bytes, WORK_STATE_PATH);
245
+ assertJsonBytes(bytes, relPath);
240
246
  state = JSON.parse(bytes.toString("utf8"));
241
247
  } catch (error) {
242
- throw new WorkStateError(`Unable to parse ${WORK_STATE_PATH}: ${error.message}`);
248
+ throw new WorkStateError(`Unable to parse ${relPath}: ${error.message}`);
243
249
  }
244
250
  try {
245
251
  return await validateStoredState(state, packageRoot);
@@ -263,20 +269,25 @@ export async function readContractFingerprint(target, contractFile) {
263
269
  return { path: contractFile, fingerprint: contractFingerprint(contract) };
264
270
  }
265
271
 
266
- export async function writeWorkState(target, state, { dryRun = false, packageRoot = getPackageRoot() } = {}) {
272
+ export async function writeWorkState(target, state, options = {}) {
273
+ const packageRoot = options?.packageRoot ?? getPackageRoot();
274
+ const dryRun = options?.dryRun ?? false;
275
+ const relPath = options?.statePath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "state") : WORK_STATE_PATH);
276
+
267
277
  await validateStoredState(state, packageRoot);
268
- await assertSafePath(target, WORK_STATE_PATH);
269
- const statePath = ensureWithin(target, WORK_STATE_PATH);
278
+ await assertSafePath(target, relPath);
279
+ const statePath = ensureWithin(target, relPath);
270
280
  await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}\n`, { dryRun });
271
281
  return state;
272
282
  }
273
283
 
274
- export async function clearWorkState(target) {
275
- await assertSafePath(target, WORK_STATE_PATH);
276
- const statePath = ensureWithin(target, WORK_STATE_PATH);
277
- if (!(await fileExists(statePath))) return { removed: false, path: WORK_STATE_PATH };
284
+ export async function clearWorkState(target, options = {}) {
285
+ const relPath = options?.statePath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "state") : WORK_STATE_PATH);
286
+ await assertSafePath(target, relPath);
287
+ const statePath = ensureWithin(target, relPath);
288
+ if (!(await fileExists(statePath))) return { removed: false, path: relPath };
278
289
  await unlink(statePath);
279
- return { removed: true, path: WORK_STATE_PATH };
290
+ return { removed: true, path: relPath };
280
291
  }
281
292
 
282
293
  export async function readRequiredArtifactFingerprints(target, artifacts) {