@cassiomc1/forgeloop 1.2.1 → 1.2.3

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 (65) hide show
  1. package/.cursor/rules/project-loop.mdc +1 -1
  2. package/.github/copilot-instructions.md +1 -1
  3. package/AGENTS.md +1 -1
  4. package/CLAUDE.md +1 -1
  5. package/DOCS_INDEX.md +3 -0
  6. package/ENG/design-code-eng.md +59 -0
  7. package/ENG/premium-sites-studio-eng.md +28 -0
  8. package/LOOP_ENGINEERING.md +23 -0
  9. package/LOOP_SYSTEM_DESIGN.md +9 -5
  10. package/ORCHESTRATOR_INTEGRATION.md +37 -4
  11. package/PROTOCOL_INTEGRATION.md +13 -0
  12. package/README.md +34 -2
  13. package/TERMINOLOGY.md +10 -0
  14. package/THIRD_PARTY_NOTICES.md +34 -0
  15. package/THREAT_MODEL.md +12 -1
  16. package/docs/ARTIFACT_REFERENCE.md +150 -0
  17. package/docs/CLI_REFERENCE.md +263 -30
  18. package/docs/CROSS_HARNESS_CONTINUITY.md +1 -0
  19. package/docs/DOCUMENTATION_GUIDE.md +41 -4
  20. package/docs/GETTING_STARTED.md +9 -4
  21. package/docs/RECIPES.md +31 -1
  22. package/docs/TROUBLESHOOTING.md +191 -0
  23. package/package.json +1 -1
  24. package/schemas/policy-baseline.schema.json +26 -0
  25. package/schemas/policy-discovery.schema.json +45 -0
  26. package/schemas/policy-lock.schema.json +16 -0
  27. package/schemas/policy-rules.schema.json +48 -0
  28. package/schemas/policy-snapshot.schema.json +16 -0
  29. package/src/cli.js +69 -1
  30. package/src/commands/baseline.js +120 -0
  31. package/src/commands/init.js +304 -6
  32. package/src/commands/policy-diff.js +51 -0
  33. package/src/commands/policy-discover.js +42 -0
  34. package/src/commands/policy-status.js +33 -0
  35. package/src/commands/profile-interview.js +50 -0
  36. package/src/commands/reconcile-closure.js +49 -0
  37. package/src/commands/rule-verify.js +36 -0
  38. package/src/commands/validate-receipt.js +38 -3
  39. package/src/core/artifact-registry.js +60 -0
  40. package/src/core/audit.js +24 -0
  41. package/src/core/cli-command-definitions.js +114 -7
  42. package/src/core/cli-metadata.js +1 -1
  43. package/src/core/completion-artifacts.js +29 -3
  44. package/src/core/completion.js +101 -10
  45. package/src/core/error-codes.js +227 -0
  46. package/src/core/events.js +22 -0
  47. package/src/core/execution-prerequisites.js +38 -20
  48. package/src/core/execution.js +20 -3
  49. package/src/core/native-adapters.js +14 -4
  50. package/src/core/next-action-model.js +9 -0
  51. package/src/core/next-action.js +128 -82
  52. package/src/core/policy-adapters.js +276 -0
  53. package/src/core/policy-baseline.js +144 -0
  54. package/src/core/policy-diff.js +133 -0
  55. package/src/core/policy-discovery.js +225 -0
  56. package/src/core/policy-engine.js +533 -0
  57. package/src/core/policy-mutation.js +139 -0
  58. package/src/core/preflight-consistency.js +23 -15
  59. package/src/core/preflight.js +65 -1
  60. package/src/core/reconcile-closure.js +175 -0
  61. package/src/core/schema-validation.js +6 -0
  62. package/src/core/task-context.js +11 -0
  63. package/src/core/task-discovery.js +67 -1
  64. package/src/core/task-paths.js +9 -0
  65. package/src/core/templates.js +5 -0
@@ -0,0 +1,33 @@
1
+ import { evaluateTargetPolicy } from "../core/policy-engine.js";
2
+
3
+ export async function runPolicyStatus({ target = process.cwd(), packageRoot, taskId = null } = {}) {
4
+ return evaluateTargetPolicy({ target, packageRoot, taskId });
5
+ }
6
+
7
+ export function formatPolicyStatusResult(result) {
8
+ const lines = [
9
+ `FORGELOOP POLICY STATUS: ${result.status}`,
10
+ `Lock: ${result.lock?.digest ?? "none"}`,
11
+ `Rules: ${result.rules?.length ?? 0} total (Proven: ${result.provenRules}, Inert: ${result.inertRules}, Unsupported: ${result.unsupportedRules})`,
12
+ `Baseline Violations: ${result.baselineViolations}`,
13
+ `New Violations: ${result.newViolations?.length ?? 0}`,
14
+ `Drift: ${result.drift?.detected ? `DETECTED (${result.drift.classification})` : "none"}`,
15
+ ];
16
+
17
+ if (result.errors?.length > 0) {
18
+ lines.push("Errors:");
19
+ for (const err of result.errors) {
20
+ lines.push(` - ${err.code}: ${err.why || err.message || err.ruleId}`);
21
+ if (err.fix) lines.push(` Fix: ${err.fix}`);
22
+ }
23
+ }
24
+
25
+ if (result.warnings?.length > 0) {
26
+ lines.push("Warnings:");
27
+ for (const warn of result.warnings) {
28
+ lines.push(` - ${warn.code}: ${warn.message || warn.why}`);
29
+ }
30
+ }
31
+
32
+ return `${lines.join("\n")}\n`;
33
+ }
@@ -0,0 +1,50 @@
1
+ import { discoverPolicy } from "../core/policy-discovery.js";
2
+
3
+ export async function runProfileInterview({
4
+ target = process.cwd(),
5
+ packageRoot,
6
+ dryRun = false,
7
+ } = {}) {
8
+ const discovery = await discoverPolicy({ target });
9
+ return {
10
+ status: "COMPLETE",
11
+ mode: "OPTIONAL_INTERVIEW",
12
+ dryRun,
13
+ questions: [
14
+ {
15
+ topic: "languages",
16
+ detected: discovery.languages,
17
+ recommendation: discovery.languages.join(", ") || "none",
18
+ },
19
+ {
20
+ topic: "testing",
21
+ detected: discovery.testing.detected,
22
+ framework: discovery.testing.framework,
23
+ confidence: discovery.testing.confidence,
24
+ },
25
+ {
26
+ topic: "linting",
27
+ detected: discovery.linting.detected,
28
+ tool: discovery.linting.tool,
29
+ confidence: discovery.linting.confidence,
30
+ },
31
+ {
32
+ topic: "architecture",
33
+ detected: discovery.architecture.value,
34
+ confidence: discovery.architecture.confidence,
35
+ },
36
+ ],
37
+ discovery,
38
+ };
39
+ }
40
+
41
+ export function formatProfileInterviewResult(result) {
42
+ const lines = [
43
+ "FORGELOOP PROFILE INTERVIEW (OPTIONAL):",
44
+ `Languages: ${result.discovery?.languages?.join(", ") || "none detected"}`,
45
+ `Testing: ${result.discovery?.testing?.detected ? `${result.discovery.testing.framework} [${result.discovery.testing.confidence}]` : "none"}`,
46
+ `Linting: ${result.discovery?.linting?.detected ? `${result.discovery.linting.tool} [${result.discovery.linting.confidence}]` : "none"}`,
47
+ `Architecture: ${result.discovery?.architecture?.value ? `${result.discovery.architecture.value} [${result.discovery.architecture.confidence}]` : "unknown"}`,
48
+ ];
49
+ return `${lines.join("\n")}\n`;
50
+ }
@@ -0,0 +1,49 @@
1
+ import { runReconcileClosure } from "../core/reconcile-closure.js";
2
+ import { withTaskMutation } from "../core/task-command.js";
3
+
4
+ export async function reconcileClosure({
5
+ target,
6
+ packageRoot,
7
+ taskId,
8
+ task,
9
+ checkId,
10
+ checkRequirement,
11
+ checkDetails,
12
+ commandArgv,
13
+ authorityContext,
14
+ runtimeContext,
15
+ }) {
16
+ return withTaskMutation(
17
+ target,
18
+ { taskId: taskId ?? task, packageRoot },
19
+ "reconcile-closure",
20
+ async (ctx) => {
21
+ return runReconcileClosure({
22
+ target,
23
+ packageRoot,
24
+ taskId: ctx?.taskId ?? null,
25
+ checkId,
26
+ requirement: checkRequirement,
27
+ argv: commandArgv,
28
+ details: checkDetails,
29
+ authorityContext,
30
+ runtimeContext,
31
+ });
32
+ },
33
+ { explicitRequired: true },
34
+ );
35
+ }
36
+
37
+ export function formatReconcileClosureResult(result) {
38
+ const previous = result.previousRepositoryFingerprint;
39
+ return [
40
+ "FORGELOOP CHECKPOINT RECONCILED",
41
+ `task: ${result.taskId}`,
42
+ `check: ${result.checkId} (passed)`,
43
+ `execution: ${result.executionId}`,
44
+ `previous: ${previous?.branch ?? "unknown"} @ ${previous?.head ?? "unknown"}`,
45
+ `current: ${result.repositoryFingerprint?.branch ?? "unknown"} @ ${result.repositoryFingerprint?.head ?? "unknown"}`,
46
+ `event: ${result.event}`,
47
+ "",
48
+ ].join("\n");
49
+ }
@@ -0,0 +1,36 @@
1
+ import { loadEffectiveRules } from "../core/policy-engine.js";
2
+ import { verifyRuleMutation } from "../core/policy-mutation.js";
3
+
4
+ export async function runRuleVerify({
5
+ target = process.cwd(),
6
+ packageRoot,
7
+ rule = null,
8
+ } = {}) {
9
+ const rules = await loadEffectiveRules(target, packageRoot);
10
+ const targetRules = rule ? rules.filter((r) => r.id === rule) : rules;
11
+
12
+ const verifications = [];
13
+ for (const r of targetRules) {
14
+ const res = await verifyRuleMutation({ target, rule: r });
15
+ verifications.push(res);
16
+ }
17
+
18
+ const allProven = verifications.every((v) => v.status === "PROVEN" || v.status === "UNSUPPORTED");
19
+
20
+ return {
21
+ status: allProven ? "VALID" : "UNPROVEN",
22
+ verifications,
23
+ };
24
+ }
25
+
26
+ export function formatRuleVerifyResult(result) {
27
+ const lines = [
28
+ `FORGELOOP RULE VERIFICATION: ${result.status}`,
29
+ ];
30
+ for (const v of result.verifications ?? []) {
31
+ lines.push(` - ${v.ruleId}: ${v.status} (Mutation: ${v.mutation ?? "none"}, Expected: ${v.expected ?? "n/a"}, Observed: ${v.observed ?? "n/a"})`);
32
+ if (v.why) lines.push(` Why: ${v.why}`);
33
+ if (v.fix && v.status !== "PROVEN") lines.push(` Fix: ${v.fix}`);
34
+ }
35
+ return `${lines.join("\n")}\n`;
36
+ }
@@ -2,9 +2,10 @@ import { assertSafePath, ensureWithin, readBytes } from "../core/filesystem.js";
2
2
  import { validateReceipt } from "../core/receipt.js";
3
3
  import { assertJsonBytes, assertJsonLimits } from "../core/json-safety.js";
4
4
  import { ARTIFACT_PATHS } from "../core/artifacts.js";
5
+ import { taskArtifactPath } from "../core/task-paths.js";
6
+ import { withResolvedTask } from "../core/task-command.js";
5
7
 
6
- export async function runValidateReceipt({ target, packageRoot, file }) {
7
- const relativeFile = file ?? ARTIFACT_PATHS.receipt;
8
+ async function validateReceiptFile(target, packageRoot, relativeFile) {
8
9
  await assertSafePath(target, relativeFile);
9
10
  const receiptPath = ensureWithin(target, relativeFile);
10
11
  let receipt;
@@ -16,5 +17,39 @@ export async function runValidateReceipt({ target, packageRoot, file }) {
16
17
  } catch (error) {
17
18
  throw new Error(`Unable to parse receipt ${relativeFile}: ${error.message}`);
18
19
  }
19
- return validateReceipt(receipt, packageRoot);
20
+ try {
21
+ return await validateReceipt(receipt, packageRoot);
22
+ } catch (error) {
23
+ throw new Error(`Invalid receipt ${relativeFile}: ${error.message}`);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Validates an execution receipt with deterministic resolution precedence:
29
+ * 1. explicit `--file` validates exactly that relative file;
30
+ * 2. explicit or context-resolved `--task` validates that task's namespaced
31
+ * `.forgeloop/task-state/<taskKey>/execution-receipt.json`;
32
+ * 3. a single active task is resolved automatically through the shared
33
+ * task-command resolver;
34
+ * 4. when no task descriptors exist, the legacy singleton
35
+ * `.forgeloop/execution-receipt.json` compatibility path is preserved.
36
+ * Multiple active tasks without `--task`/`--file` fail with E_TASK_AMBIGUOUS
37
+ * through the shared resolver instead of silently falling back to the legacy
38
+ * singleton.
39
+ */
40
+ export async function runValidateReceipt({
41
+ target,
42
+ packageRoot,
43
+ file = null,
44
+ taskId = null,
45
+ } = {}) {
46
+ if (file) {
47
+ return validateReceiptFile(target, packageRoot, file);
48
+ }
49
+ return withResolvedTask(target, { taskId, packageRoot }, async (ctx) => {
50
+ const relativeFile = ctx
51
+ ? taskArtifactPath(ctx.taskId, "receipt")
52
+ : ARTIFACT_PATHS.receipt;
53
+ return validateReceiptFile(target, packageRoot, relativeFile);
54
+ });
20
55
  }
@@ -163,4 +163,64 @@ export const ARTIFACT_REGISTRY = Object.freeze({
163
163
  isPersisted: true,
164
164
  description: "Attested command execution provenance recording argv, cwd, resolution, status, and exit code.",
165
165
  }),
166
+ policyRules: Object.freeze({
167
+ key: "policyRules",
168
+ scope: "PROJECT",
169
+ path: PROJECT_ARTIFACT_PATHS.policyRules,
170
+ schema: "policy-rules",
171
+ owner: "OPERATOR_OR_AGENT",
172
+ mutability: "MUTABLE_CONFIGURATION",
173
+ trustRole: "POLICY_SPECIFICATION",
174
+ isPublic: true,
175
+ isPersisted: true,
176
+ description: "Executable policy rule definitions declaring severities, checks, why, and fix guidance.",
177
+ }),
178
+ policyDiscovery: Object.freeze({
179
+ key: "policyDiscovery",
180
+ scope: "PROJECT",
181
+ path: PROJECT_ARTIFACT_PATHS.policyDiscovery,
182
+ schema: "policy-discovery",
183
+ owner: "PROTOCOL_GENERATED",
184
+ mutability: "MUTABLE_ON_DISCOVERY",
185
+ trustRole: "DISCOVERED_POLICY_SPECIFICATION",
186
+ isPublic: true,
187
+ isPersisted: true,
188
+ description: "Deterministic repository policy discovery report with confidence scoring.",
189
+ }),
190
+ policyBaseline: Object.freeze({
191
+ key: "policyBaseline",
192
+ scope: "PROJECT",
193
+ path: PROJECT_ARTIFACT_PATHS.policyBaseline,
194
+ schema: "policy-baseline",
195
+ owner: "PROTOCOL_GENERATED_OR_OPERATOR",
196
+ mutability: "MONOTONIC_RATCHET_DOWN",
197
+ trustRole: "BROWNFIELD_BASELINE",
198
+ isPublic: true,
199
+ isPersisted: true,
200
+ description: "Brownfield policy baseline recording existing debt with sha256 violation fingerprints.",
201
+ }),
202
+ policyLock: Object.freeze({
203
+ key: "policyLock",
204
+ scope: "PROJECT",
205
+ path: PROJECT_ARTIFACT_PATHS.policyLock,
206
+ schema: "policy-lock",
207
+ owner: "PROTOCOL_GENERATED",
208
+ mutability: "ATOMIC_DIGEST_COMPILATION",
209
+ trustRole: "POLICY_INTEGRITY_LOCK",
210
+ isPublic: true,
211
+ isPersisted: true,
212
+ description: "Canonical policy lockfile holding cryptographic digest of effective rules and baseline.",
213
+ }),
214
+ policySnapshot: Object.freeze({
215
+ key: "policySnapshot",
216
+ scope: "TASK",
217
+ path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.policySnapshot}`,
218
+ schema: "policy-snapshot",
219
+ owner: "PROTOCOL_GENERATED",
220
+ mutability: "MUTABLE_BEFORE_EXECUTION",
221
+ trustRole: "TASK_POLICY_ATTESTATION",
222
+ isPublic: true,
223
+ isPersisted: true,
224
+ description: "Task-scoped policy snapshot binding task activation to effective policy digest.",
225
+ }),
166
226
  });
package/src/core/audit.js CHANGED
@@ -131,6 +131,29 @@ export async function evaluateAudit({
131
131
  : blocked
132
132
  ? "INCOMPLETE"
133
133
  : "INVALID";
134
+ let policyStatus = null;
135
+ const { detectPolicyCapability, evaluateTargetPolicy } = await import("./policy-engine.js");
136
+ const policyCapability = await detectPolicyCapability(target, packageRoot);
137
+ if (policyCapability === "AVAILABLE") {
138
+ try {
139
+ const policyEval = await evaluateTargetPolicy({ target, packageRoot, taskId });
140
+ policyStatus = {
141
+ status: policyEval.status,
142
+ provenRules: policyEval.provenRules,
143
+ inertRules: policyEval.inertRules,
144
+ unsupportedRules: policyEval.unsupportedRules,
145
+ baselineViolations: policyEval.baselineViolations,
146
+ drift: policyEval.drift?.detected ?? false,
147
+ };
148
+ } catch {
149
+ policyStatus = { status: "INVALID", provenRules: 0, inertRules: 0, unsupportedRules: 0, baselineViolations: 0, drift: false };
150
+ }
151
+ } else if (policyCapability === "INVALID") {
152
+ policyStatus = { status: "INVALID", provenRules: 0, inertRules: 0, unsupportedRules: 0, baselineViolations: 0, drift: false };
153
+ } else {
154
+ policyStatus = { status: "NOT_APPLICABLE", provenRules: 0, inertRules: 0, unsupportedRules: 0, baselineViolations: 0, drift: false };
155
+ }
156
+
134
157
  return {
135
158
  schemaVersion: 1,
136
159
  protocolVersion: PROTOCOL_VERSION,
@@ -142,6 +165,7 @@ export async function evaluateAudit({
142
165
  status: manifest ? "ready" : manifestError ? "invalid" : "missing",
143
166
  manifest: Boolean(manifest),
144
167
  },
168
+ policy: policyStatus,
145
169
  completion,
146
170
  changedPaths,
147
171
  publicationStatus: completion.publicationStatus,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Canonical, declarative definition of all 36 ForgeLoop CLI commands.
2
+ * Canonical, declarative definition of all 42 ForgeLoop CLI commands.
3
3
  * This is the machine source of truth for CLI option parsing, help text,
4
4
  * metadata, documentation generation, and conformance validation.
5
5
  *
@@ -64,7 +64,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
64
64
  mutation: "MUTATING",
65
65
  options: Object.freeze({
66
66
  ...CLI_COMMON_OPTIONS,
67
- "--dry-run": Object.freeze({ targetKey: "dryRun", parseType: "boolean", takesValue: false, description: "show planned writes without changing files" }),
67
+ "--dry-run": Object.freeze({ targetKey: "dryRun", parseType: "boolean", takesValue: false, description: "perform deterministic init planning and conflict detection without writing" }),
68
68
  }),
69
69
  writes: [".forgeloop/*", "AGENTS.md", "CLAUDE.md", ".cursor/rules/project-loop.mdc", ".github/copilot-instructions.md"],
70
70
  removes: [],
@@ -93,7 +93,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
93
93
  mutation: "MUTATING",
94
94
  options: Object.freeze({
95
95
  ...CLI_COMMON_OPTIONS,
96
- "--dry-run": Object.freeze({ targetKey: "dryRun", parseType: "boolean", takesValue: false, description: "show planned writes without changing files" }),
96
+ "--dry-run": Object.freeze({ targetKey: "dryRun", parseType: "boolean", takesValue: false, description: "perform deterministic update planning and conflict detection without writing" }),
97
97
  }),
98
98
  writes: [".forgeloop/*", "AGENTS.md", "CLAUDE.md", ".cursor/rules/project-loop.mdc", ".github/copilot-instructions.md"],
99
99
  removes: [],
@@ -426,8 +426,22 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
426
426
  mayExecuteExternalProcess: false,
427
427
  description: "Evaluates active task state against named enterprise policy packs.",
428
428
  }),
429
- bundle: Object.freeze({
430
- name: "bundle",
429
+ "policy-discover": Object.freeze({
430
+ name: "policy-discover",
431
+ category: "policy-audit",
432
+ mutation: "MUTATING",
433
+ options: Object.freeze({
434
+ ...CLI_COMMON_OPTIONS,
435
+ "--write": Object.freeze({ targetKey: "write", parseType: "boolean", takesValue: false, description: "persist discovered policy to .forgeloop/policy/discovery.json" }),
436
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
437
+ }),
438
+ writes: [".forgeloop/policy/discovery.json", ".forgeloop/policy/policy.lock"],
439
+ removes: [],
440
+ mayExecuteExternalProcess: false,
441
+ description: "Discovers repository policy facts and candidate rules deterministically.",
442
+ }),
443
+ "policy-status": Object.freeze({
444
+ name: "policy-status",
431
445
  category: "policy-audit",
432
446
  mutation: "READ_ONLY",
433
447
  options: Object.freeze({
@@ -438,6 +452,80 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
438
452
  writes: [],
439
453
  removes: [],
440
454
  mayExecuteExternalProcess: false,
455
+ description: "Evaluates repository and task state against effective policy rules and baseline.",
456
+ }),
457
+ "policy-diff": Object.freeze({
458
+ name: "policy-diff",
459
+ category: "policy-audit",
460
+ mutation: "READ_ONLY",
461
+ options: Object.freeze({
462
+ ...CLI_COMMON_OPTIONS,
463
+ ...CLI_TASK_OPTION,
464
+ "--before": Object.freeze({ targetKey: "before", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--before requires a path", description: "path to before policy JSON" }),
465
+ "--after": Object.freeze({ targetKey: "after", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--after requires a path", description: "path to after policy JSON" }),
466
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
467
+ }),
468
+ writes: [],
469
+ removes: [],
470
+ mayExecuteExternalProcess: false,
471
+ description: "Performs semantic diffing between policy versions to detect tightening or weakening.",
472
+ }),
473
+ "rule-verify": Object.freeze({
474
+ name: "rule-verify",
475
+ category: "policy-audit",
476
+ mutation: "READ_ONLY",
477
+ options: Object.freeze({
478
+ ...CLI_COMMON_OPTIONS,
479
+ "--rule": Object.freeze({ targetKey: "rule", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--rule requires an ID", description: "verify a specific policy rule ID" }),
480
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
481
+ }),
482
+ writes: [],
483
+ removes: [],
484
+ mayExecuteExternalProcess: false,
485
+ description: "Verifies policy rules against mutation fixtures to prove detector efficacy.",
486
+ }),
487
+ baseline: Object.freeze({
488
+ name: "baseline",
489
+ category: "policy-audit",
490
+ mutation: "MUTATING",
491
+ options: Object.freeze({
492
+ ...CLI_COMMON_OPTIONS,
493
+ "--record": Object.freeze({ targetKey: "record", parseType: "boolean", takesValue: false, description: "record current violations as brownfield baseline" }),
494
+ "--update": Object.freeze({ targetKey: "update", parseType: "boolean", takesValue: false, description: "ratchet baseline downward by removing resolved violations" }),
495
+ "--policy-reset-authorized": Object.freeze({ targetKey: "policyResetAuthorized", parseType: "boolean", takesValue: false, description: "explicit operator authority to re-record baseline during active tasks" }),
496
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
497
+ }),
498
+ writes: [".forgeloop/policy/baseline.json", ".forgeloop/policy/policy.lock"],
499
+ removes: [],
500
+ mayExecuteExternalProcess: false,
501
+ description: "Manages brownfield policy baseline violations with monotonic downward ratcheting.",
502
+ }),
503
+ "profile-interview": Object.freeze({
504
+ name: "profile-interview",
505
+ category: "diagnostics",
506
+ mutation: "READ_ONLY",
507
+ options: Object.freeze({
508
+ ...CLI_COMMON_OPTIONS,
509
+ "--dry-run": Object.freeze({ targetKey: "dryRun", parseType: "boolean", takesValue: false, description: "show planned interview questions without changing files" }),
510
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
511
+ }),
512
+ writes: [],
513
+ removes: [],
514
+ mayExecuteExternalProcess: false,
515
+ description: "Optional interactive or dry-run interview to refine project profile facts.",
516
+ }),
517
+ bundle: Object.freeze({
518
+ name: "bundle",
519
+ category: "policy-audit",
520
+ mutation: "MUTATING",
521
+ options: Object.freeze({
522
+ ...CLI_COMMON_OPTIONS,
523
+ ...CLI_TASK_OPTION,
524
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
525
+ }),
526
+ writes: [".forgeloop/tasks/<taskId>"],
527
+ removes: [],
528
+ mayExecuteExternalProcess: false,
441
529
  description: "Exports current task artifacts into a portable task bundle archive.",
442
530
  }),
443
531
  inspect: Object.freeze({
@@ -498,13 +586,32 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
498
586
  mayExecuteExternalProcess: false,
499
587
  description: "Removes work-state.json for the active task only, preserving sibling contract, routing, and ledger files.",
500
588
  }),
589
+ "reconcile-closure": Object.freeze({
590
+ name: "reconcile-closure",
591
+ category: "lifecycle",
592
+ mutation: "MUTATING",
593
+ options: Object.freeze({
594
+ ...CLI_COMMON_OPTIONS,
595
+ ...CLI_TASK_OPTION,
596
+ "--id": Object.freeze({ targetKey: "checkId", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--id requires a check ID", description: "contract verification item id used as evidence" }),
597
+ "--requirement": Object.freeze({ targetKey: "checkRequirement", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--requirement requires an evidence target", description: "exact contract verification item requirement text" }),
598
+ "--details": Object.freeze({ targetKey: "checkDetails", parseType: "json-object", takesValue: true, valueName: "json", missingValueMessage: "--details requires a JSON object", description: "additional structured execution details" }),
599
+ "--": Object.freeze({ targetKey: "commandArgv", parseType: "argv", takesValue: true, valueName: "argv...", description: "exact command argv to execute as objective-satisfaction evidence" }),
600
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
601
+ }),
602
+ writes: [".forgeloop/task-state/<taskKey>/executions/exec-<id>.json", ".forgeloop/task-state/<taskKey>/work-state.json", ".forgeloop/task-state/<taskKey>/events.ndjson"],
603
+ removes: [],
604
+ mayExecuteExternalProcess: true,
605
+ description: "Refreshes the work-state checkpoint of an EXECUTING task whose objective is already satisfied in the current repository, after contract-bound executed evidence, so canonical completion can proceed.",
606
+ }),
501
607
  "validate-receipt": Object.freeze({
502
608
  name: "validate-receipt",
503
609
  category: "verification",
504
610
  mutation: "READ_ONLY",
505
611
  options: Object.freeze({
506
612
  ...CLI_COMMON_OPTIONS,
507
- "--file": Object.freeze({ targetKey: "file", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--file requires a path", description: "receipt file relative to target" }),
613
+ ...CLI_TASK_OPTION,
614
+ "--file": Object.freeze({ targetKey: "file", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--file requires a path", description: "receipt file relative to target (overrides task-based receipt resolution)" }),
508
615
  "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
509
616
  }),
510
617
  writes: [],
@@ -544,7 +651,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
544
651
  "--contract-file": Object.freeze({ targetKey: "contractFile", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--contract-file requires a path", description: "path to initial contract file" }),
545
652
  "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
546
653
  }),
547
- writes: [".forgeloop/task-state/<taskKey>/task.json", ".forgeloop/task-state/<taskKey>/current-contract.json"],
654
+ writes: [".forgeloop/task-state/<taskKey>/task.json", ".forgeloop/task-state/<taskKey>/contract.json"],
548
655
  removes: [],
549
656
  mayExecuteExternalProcess: false,
550
657
  description: "Creates a new task descriptor and initializes its isolated task state namespace.",
@@ -1,7 +1,7 @@
1
1
  import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
2
2
 
3
3
  /**
4
- * Canonical metadata for all 27 ForgeLoop CLI commands derived directly
4
+ * Canonical metadata for all 43 ForgeLoop CLI commands derived directly
5
5
  * from CLI_COMMAND_DEFINITIONS to guarantee zero divergence.
6
6
  */
7
7
  export const CLI_COMMAND_METADATA = Object.freeze(
@@ -26,6 +26,32 @@ import { readTaskDescriptor } from "./task-descriptor.js";
26
26
  import { assertClaimsCoverChangedPaths } from "./task-scope.js";
27
27
  import { discoverTasks } from "./task-discovery.js";
28
28
 
29
+ /**
30
+ * Canonical terminal-result types shared by runtime validation, tests, and
31
+ * documentation conformance. Keep these single-sourced; do not re-declare
32
+ * the same type lists in validators or tests.
33
+ */
34
+ export const TERMINAL_RESULT_TYPES = Object.freeze([
35
+ "PUBLICATION",
36
+ "PRODUCTION_READINESS",
37
+ ]);
38
+
39
+ /**
40
+ * Canonical terminal-status sets shared by runtime validation, tests, and
41
+ * documentation conformance. Keep these single-sourced; do not re-declare
42
+ * the same status lists in validators or tests.
43
+ */
44
+ export const PUBLICATION_STATUSES = Object.freeze([
45
+ "committed",
46
+ "pushed",
47
+ "published",
48
+ "deployed",
49
+ ]);
50
+ export const PRODUCTION_READINESS_STATUSES = Object.freeze([
51
+ "ready",
52
+ "blocked",
53
+ ]);
54
+
29
55
  function artifactError(code, message, artifacts = []) {
30
56
  const error = new Error(message);
31
57
  error.code = code;
@@ -706,13 +732,13 @@ export async function recordTerminalResult({
706
732
  if (!target || !requirement || !type || !status || !source || !result) {
707
733
  throw artifactError("E_CHECK_INVALID", "record-terminal-result requires target, requirement, type, status, source, and result", [ARTIFACT_PATHS.state]);
708
734
  }
709
- if (!["PUBLICATION", "PRODUCTION_READINESS"].includes(type)) {
735
+ if (!TERMINAL_RESULT_TYPES.includes(type)) {
710
736
  throw artifactError("E_FUTURE_TERMINAL_EVIDENCE", `record-terminal-result does not support type ${type}`, [ARTIFACT_PATHS.state]);
711
737
  }
712
- if (type === "PUBLICATION" && !["committed", "pushed", "published", "deployed"].includes(status)) {
738
+ if (type === "PUBLICATION" && !PUBLICATION_STATUSES.includes(status)) {
713
739
  throw artifactError("E_CHECK_INVALID", `Invalid publication status for record-terminal-result: ${status}`, [ARTIFACT_PATHS.state]);
714
740
  }
715
- if (type === "PRODUCTION_READINESS" && !["ready", "blocked"].includes(status)) {
741
+ if (type === "PRODUCTION_READINESS" && !PRODUCTION_READINESS_STATUSES.includes(status)) {
716
742
  throw artifactError("E_CHECK_INVALID", `Invalid production readiness status for record-terminal-result: ${status}`, [ARTIFACT_PATHS.state]);
717
743
  }
718
744