@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
package/src/cli.js CHANGED
@@ -21,10 +21,17 @@ import { formatCompleteResult, runComplete } from "./commands/complete.js";
21
21
  import { formatAuditResult, runAudit } from "./commands/audit.js";
22
22
  import { formatReportResult, runReport } from "./commands/report.js";
23
23
  import { formatPolicyResult, runPolicy } from "./commands/policy.js";
24
+ import { formatPolicyDiscoverResult, runPolicyDiscover } from "./commands/policy-discover.js";
25
+ import { formatPolicyStatusResult, runPolicyStatus } from "./commands/policy-status.js";
26
+ import { formatPolicyDiffResult, runPolicyDiff } from "./commands/policy-diff.js";
27
+ import { formatRuleVerifyResult, runRuleVerify } from "./commands/rule-verify.js";
28
+ import { formatBaselineResult, runBaseline } from "./commands/baseline.js";
29
+ import { formatProfileInterviewResult, runProfileInterview } from "./commands/profile-interview.js";
24
30
  import { formatBundleResult, runBundle } from "./commands/bundle.js";
25
31
  import { formatPrepareCompletionResult, runPrepareCompletion } from "./commands/prepare-completion.js";
26
32
  import { formatRecordCheckResult, runRecordCheck } from "./commands/record-check.js";
27
33
  import { formatRunCheckResult, runCheck } from "./commands/run-check.js";
34
+ import { formatReconcileClosureResult, reconcileClosure } from "./commands/reconcile-closure.js";
28
35
  import { formatRecordTerminalResult, runRecordTerminalResult } from "./commands/record-terminal-result.js";
29
36
  import { formatRecordDiagnosisResult, runRecordDiagnosis } from "./commands/record-diagnosis.js";
30
37
  import { formatProgressResult, runProgress } from "./commands/progress.js";
@@ -339,6 +346,18 @@ export function validateCliSemantics({ command, options } = {}) {
339
346
  throw new Error("run-check requires -- followed by an exact command argv");
340
347
  }
341
348
  }
349
+ if (command === "reconcile-closure" && !options.help) {
350
+ if (!options.task) throw new Error("reconcile-closure requires --task");
351
+ if (!options.checkId) throw new Error("reconcile-closure requires --id");
352
+ if (!options.checkRequirement) throw new Error("reconcile-closure requires --requirement");
353
+ if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
354
+ || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
355
+ throw new Error("reconcile-closure accepts only --id, --requirement, --details, and -- <argv>");
356
+ }
357
+ if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
358
+ throw new Error("reconcile-closure requires -- followed by an exact command argv");
359
+ }
360
+ }
342
361
  }
343
362
 
344
363
  export function parseArgs(argv) {
@@ -553,6 +572,42 @@ export const COMMAND_HANDLERS = Object.freeze({
553
572
  console.log(options.json ? JSON.stringify(result, null, 2) : formatPolicyResult(result));
554
573
  return 0;
555
574
  },
575
+ "policy-discover": async ({ target, packageRoot, options }) => {
576
+ const result = await runPolicyDiscover({ target, packageRoot, write: options.write });
577
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatPolicyDiscoverResult(result));
578
+ return 0;
579
+ },
580
+ "policy-status": async ({ target, packageRoot, options }) => {
581
+ const result = await runPolicyStatus({ target, packageRoot, taskId: options.task });
582
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatPolicyStatusResult(result));
583
+ return result.status === "VALID" ? 0 : 1;
584
+ },
585
+ "policy-diff": async ({ target, packageRoot, options }) => {
586
+ const result = await runPolicyDiff({ target, packageRoot, taskId: options.task, before: options.before, after: options.after });
587
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatPolicyDiffResult(result));
588
+ return 0;
589
+ },
590
+ "rule-verify": async ({ target, packageRoot, options }) => {
591
+ const result = await runRuleVerify({ target, packageRoot, rule: options.rule });
592
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatRuleVerifyResult(result));
593
+ return result.status === "VALID" ? 0 : 1;
594
+ },
595
+ baseline: async ({ target, packageRoot, options }) => {
596
+ const result = await runBaseline({
597
+ target,
598
+ packageRoot,
599
+ record: options.record,
600
+ update: options.update,
601
+ policyResetAuthorized: options.policyResetAuthorized,
602
+ });
603
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatBaselineResult(result));
604
+ return 0;
605
+ },
606
+ "profile-interview": async ({ target, packageRoot, options }) => {
607
+ const result = await runProfileInterview({ target, packageRoot, dryRun: options.dryRun });
608
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatProfileInterviewResult(result));
609
+ return 0;
610
+ },
556
611
  bundle: async ({ target, packageRoot, options }) => {
557
612
  const result = await runBundle({ target, packageRoot, taskId: options.task });
558
613
  console.log(options.json ? JSON.stringify(result, null, 2) : formatBundleResult(result));
@@ -564,7 +619,7 @@ export const COMMAND_HANDLERS = Object.freeze({
564
619
  return result.ok ? 0 : 1;
565
620
  },
566
621
  "validate-receipt": async ({ target, packageRoot, options }) => {
567
- const result = await runValidateReceipt({ target, packageRoot, file: options.file });
622
+ const result = await runValidateReceipt({ target, packageRoot, file: options.file, taskId: options.task });
568
623
  console.log(options.json ? JSON.stringify(result, null, 2) : "valid: execution receipt");
569
624
  return 0;
570
625
  },
@@ -640,6 +695,19 @@ export const COMMAND_HANDLERS = Object.freeze({
640
695
  console.log(options.json ? JSON.stringify(result, null, 2) : formatTaskUnlockResult(result));
641
696
  return 0;
642
697
  },
698
+ "reconcile-closure": async ({ target, packageRoot, options }) => {
699
+ const result = await reconcileClosure({
700
+ target,
701
+ packageRoot,
702
+ taskId: options.task,
703
+ checkId: options.checkId,
704
+ checkRequirement: options.checkRequirement,
705
+ checkDetails: options.checkDetails,
706
+ commandArgv: options.commandArgv,
707
+ });
708
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatReconcileClosureResult(result));
709
+ return 0;
710
+ },
643
711
  update: async ({ target, packageRoot, packageVersion, options }) => {
644
712
  const result = await runUpdate({ target, dryRun: options.dryRun, packageRoot, packageVersion });
645
713
  printActions(result.actions);
@@ -0,0 +1,120 @@
1
+ import {
2
+ computePolicyLockData,
3
+ evaluateTargetPolicy,
4
+ loadEffectiveRules,
5
+ writePolicyLock,
6
+ readTaskPolicySnapshot,
7
+ } from "../core/policy-engine.js";
8
+ import {
9
+ createBaselineFromViolations,
10
+ readBaseline,
11
+ writeBaseline,
12
+ } from "../core/policy-baseline.js";
13
+ import { discoverTasks } from "../core/task-discovery.js";
14
+
15
+ function baselineError(code, message) {
16
+ const err = new Error(message);
17
+ err.code = code;
18
+ return err;
19
+ }
20
+
21
+ export async function runBaseline({
22
+ target = process.cwd(),
23
+ packageRoot,
24
+ record = false,
25
+ update = false,
26
+ policyResetAuthorized = false,
27
+ } = {}) {
28
+ const policyEval = await evaluateTargetPolicy({ target, packageRoot });
29
+ let baseline = await readBaseline(target, packageRoot);
30
+
31
+ const tasks = await discoverTasks(target, packageRoot);
32
+ let hasActivePolicyTask = false;
33
+ for (const t of tasks) {
34
+ if (t.healthy && t.phase && t.phase !== "COMPLETE" && t.phase !== "BLOCKED") {
35
+ const snap = await readTaskPolicySnapshot(target, t.taskId, packageRoot);
36
+ if (snap) {
37
+ hasActivePolicyTask = true;
38
+ break;
39
+ }
40
+ }
41
+ }
42
+
43
+ if (record) {
44
+ if (hasActivePolicyTask && !policyResetAuthorized) {
45
+ throw baselineError(
46
+ "E_BASELINE_RECORD_DURING_ACTIVE_TASK",
47
+ "Cannot re-record baseline during an active task with policy snapshot. Resolve new violations or use monotonic baseline --update.",
48
+ );
49
+ }
50
+
51
+ const allViolations = [];
52
+ for (const r of policyEval.rules) {
53
+ if (r.violations) allViolations.push(...r.violations);
54
+ }
55
+ baseline = createBaselineFromViolations(allViolations);
56
+ await writeBaseline(target, baseline, packageRoot);
57
+
58
+ const rules = await loadEffectiveRules(target, packageRoot);
59
+ const lock = computePolicyLockData(rules, baseline);
60
+ await writePolicyLock(target, lock, packageRoot);
61
+
62
+ return {
63
+ status: "RECORDED",
64
+ baseline,
65
+ violationCount: allViolations.length,
66
+ lock: lock.digest,
67
+ };
68
+ }
69
+
70
+ if (update && policyEval.ratchetedBaseline) {
71
+ // Monotonic check: new baseline cannot contain any fingerprint not in old baseline
72
+ const oldBaselineMap = new Map((baseline?.entries ?? []).map((e) => [e.ruleId, new Set(e.fingerprints ?? [])]));
73
+ for (const entry of policyEval.ratchetedBaseline.entries) {
74
+ const oldFps = oldBaselineMap.get(entry.ruleId) ?? new Set();
75
+ const hasExpansion = entry.fingerprints.some((fp) => !oldFps.has(fp));
76
+ if (hasExpansion) {
77
+ throw baselineError(
78
+ "E_BASELINE_EXPANSION",
79
+ `Cannot expand baseline debt for rule ${entry.ruleId}. Monotonic ratchet down allows removing debt only.`,
80
+ );
81
+ }
82
+ }
83
+
84
+ baseline = policyEval.ratchetedBaseline;
85
+ await writeBaseline(target, baseline, packageRoot);
86
+
87
+ const rules = await loadEffectiveRules(target, packageRoot);
88
+ const lock = computePolicyLockData(rules, baseline);
89
+ await writePolicyLock(target, lock, packageRoot);
90
+
91
+ return {
92
+ status: "UPDATED",
93
+ baseline,
94
+ resolvedCount: policyEval.resolvedViolations?.length ?? 0,
95
+ remainingCount: baseline.entries.reduce((sum, e) => sum + e.fingerprints.length, 0),
96
+ lock: lock.digest,
97
+ };
98
+ }
99
+
100
+ return {
101
+ status: baseline ? "VALID" : "NOT_PRESENT",
102
+ baseline,
103
+ baselinedViolations: policyEval.baselineViolations,
104
+ newViolations: policyEval.newViolations?.length ?? 0,
105
+ resolvedViolations: policyEval.resolvedViolations?.length ?? 0,
106
+ };
107
+ }
108
+
109
+ export function formatBaselineResult(result) {
110
+ const lines = [
111
+ `FORGELOOP POLICY BASELINE: ${result.status}`,
112
+ `Baselined Debt: ${result.baselinedViolations ?? (result.baseline?.entries?.reduce((sum, e) => sum + e.fingerprints.length, 0) ?? 0)}`,
113
+ `New Violations: ${result.newViolations ?? 0}`,
114
+ `Resolved Debt: ${result.resolvedViolations ?? result.resolvedCount ?? 0}`,
115
+ ];
116
+ if (result.lock) {
117
+ lines.push(`Lock: ${result.lock}`);
118
+ }
119
+ return `${lines.join("\n")}\n`;
120
+ }
@@ -6,8 +6,153 @@ import {
6
6
  writeManifest,
7
7
  } from "../core/manifest.js";
8
8
  import { readTemplateEntries } from "../core/templates.js";
9
+ import { PROJECT_ARTIFACT_PATHS } from "../core/task-paths.js";
10
+ import { isKitPath } from "../core/target-layout.js";
11
+ import { E_INIT_KIT_CONFLICT, E_POLICY_INITIALIZATION_FAILED } from "../core/error-codes.js";
9
12
 
10
- export async function runInit({ target, dryRun, packageRoot, packageVersion }) {
13
+ // Compatibility re-exports: the canonical semantic definitions live in
14
+ // src/core/error-codes.js; this keeps existing import paths working while
15
+ // ensuring there is exactly one literal source for each constant.
16
+ export { E_INIT_KIT_CONFLICT, E_POLICY_INITIALIZATION_FAILED };
17
+
18
+ /**
19
+ * Executable-policy artifacts initialized by `forgeloop init`. The manifest is
20
+ * the initialization commit marker and must never be written before these have
21
+ * been produced and verified.
22
+ */
23
+ export const POLICY_INIT_ARTIFACTS = Object.freeze([
24
+ PROJECT_ARTIFACT_PATHS.policyDiscovery,
25
+ PROJECT_ARTIFACT_PATHS.policyBaseline,
26
+ PROJECT_ARTIFACT_PATHS.policyLock,
27
+ ]);
28
+
29
+ function policyInitializationError(cause) {
30
+ const error = new Error(`Executable policy initialization failed: ${cause.message}`);
31
+ error.code = E_POLICY_INITIALIZATION_FAILED;
32
+ error.cause = cause;
33
+ error.artifacts = [...POLICY_INIT_ARTIFACTS];
34
+ return error;
35
+ }
36
+
37
+ function baselineSemanticallyEqual(a, b) {
38
+ return a?.schemaVersion === b?.schemaVersion
39
+ && JSON.stringify(a?.entries ?? []) === JSON.stringify(b?.entries ?? []);
40
+ }
41
+
42
+ function lockSemanticallyEqual(a, b) {
43
+ return a?.algorithm === b?.algorithm
44
+ && a?.digest === b?.digest
45
+ && a?.rulesDigest === b?.rulesDigest
46
+ && a?.baselineDigest === b?.baselineDigest;
47
+ }
48
+
49
+ /**
50
+ * Reconciles an existing policy artifact against the freshly computed
51
+ * canonical initialized state.
52
+ *
53
+ * - missing -> write it
54
+ * - valid + equal -> reuse (resumable output of a failed init)
55
+ * - valid + different -> not provably ForgeLoop-owned: preserve and fail
56
+ * with a deterministic conflict
57
+ * - invalid/corrupt -> fail closed; never overwrite unknown content
58
+ */
59
+ async function reconcilePolicyArtifact(target, packageRoot, readExisting, expected, { semanticEqual, label }) {
60
+ let existing = null;
61
+ try {
62
+ existing = await readExisting(target, packageRoot);
63
+ } catch (cause) {
64
+ throw policyInitializationError(
65
+ new Error(`${label} exists but is invalid and cannot be safely reconciled: ${cause.message}`),
66
+ );
67
+ }
68
+ if (existing === null) return { action: "write" };
69
+ if (semanticEqual(existing, expected)) return { action: "reuse" };
70
+ throw policyInitializationError(
71
+ new Error(`${label} does not match the canonical initialized state and is not provably ForgeLoop-owned; preserve it or remove it before re-running init`),
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Classifies an existing template destination against the shipped template.
77
+ * Shared by the pre-mutation conflict scan and the mutation loop so the two
78
+ * phases cannot drift apart:
79
+ *
80
+ * - MATCH existing bytes equal the shipped template (resumable)
81
+ * - PRESERVE_PROFILE custom PROJECT_PROFILE.md (intentionally preservable)
82
+ * - KIT_CONFLICT canonical hidden kit content differs (fail closed)
83
+ * - PRESERVE_UNOWNED pre-existing root/brownfield file (preserve, unowned)
84
+ */
85
+ export function classifyExistingInitTemplate({ entry, existingBytes }) {
86
+ const existingHash = sha256(existingBytes);
87
+ const templateHash = sha256(entry.bytes);
88
+
89
+ if (existingHash === templateHash) {
90
+ return { kind: "MATCH", existingHash, templateHash };
91
+ }
92
+ if (entry.sourcePath === "PROJECT_PROFILE.md") {
93
+ return { kind: "PRESERVE_PROFILE", existingHash, templateHash };
94
+ }
95
+ if (isKitPath(entry.relativePath)) {
96
+ return { kind: "KIT_CONFLICT", existingHash, templateHash };
97
+ }
98
+ return { kind: "PRESERVE_UNOWNED", existingHash, templateHash };
99
+ }
100
+
101
+ function kitConflictError(relativePath) {
102
+ const error = new Error(
103
+ `Canonical ForgeLoop kit file conflicts with shipped template: ${relativePath}`,
104
+ );
105
+ error.code = E_INIT_KIT_CONFLICT;
106
+ error.artifacts = [relativePath];
107
+ return error;
108
+ }
109
+
110
+ /**
111
+ * Initializes a target project with the ForgeLoop kit and executable-policy
112
+ * bootstrap, committing manifest authority LAST.
113
+ *
114
+ * Execution order:
115
+ * PHASE 1 — READ / PLAN (no writes):
116
+ * 1. validate all destination paths
117
+ * 2. read template entries
118
+ * 3. inspect the manifest
119
+ * 4. classify every existing template destination; a canonical kit
120
+ * conflict throws E_INIT_KIT_CONFLICT before ANY write
121
+ * 5. compute policy discovery, baseline, effective rules, and policy.lock
122
+ * in memory
123
+ * 6. pre-reconcile existing policy artifacts (read-only) so a policy
124
+ * conflict also fails before any write
125
+ * PHASE 2 — MUTATE:
126
+ * 7. write/reuse allowed kit files per the precomputed plan
127
+ * 8. write/reuse policy artifacts per the precomputed decisions
128
+ * 9. verify executable-policy capability and policy.lock
129
+ * 10. write manifest atomically
130
+ * 11. return success
131
+ *
132
+ * Any policy-bootstrap failure raises E_POLICY_INITIALIZATION_FAILED and
133
+ * leaves no committed manifest, so a retry reconciles already-correct files
134
+ * without manual cleanup.
135
+ *
136
+ * `init --dry-run` performs the same deterministic initialization planning and
137
+ * conflict detection as real init (existing manifest, unsafe paths, canonical
138
+ * kit conflicts, invalid or conflicting policy artifacts, and the computable
139
+ * policy state), but performs NO mutation: it does not simulate filesystem
140
+ * failures that only occur during a real write, power loss, post-write races,
141
+ * or verification of bytes that were never written.
142
+ *
143
+ * `hooks` is a deterministic failure-injection seam used only by tests:
144
+ * afterPolicyDiscovery(discovery, { target, packageRoot })
145
+ * beforeBaselineWrite({ target, packageRoot, baseline })
146
+ * beforePolicyLockWrite({ target, packageRoot, lock })
147
+ * beforeManifestWrite({ target, packageRoot, manifest, actions })
148
+ */
149
+ export async function runInit({
150
+ target,
151
+ dryRun,
152
+ packageRoot,
153
+ packageVersion,
154
+ hooks = {},
155
+ } = {}) {
11
156
  const entries = await readTemplateEntries(packageRoot);
12
157
  const existingManifest = await readManifest(target);
13
158
  if (existingManifest) {
@@ -16,20 +161,112 @@ export async function runInit({ target, dryRun, packageRoot, packageVersion }) {
16
161
  const manifest = createManifest(packageVersion);
17
162
  const actions = [];
18
163
 
19
- // Validate every destination before any write so a symlinked kit cannot
164
+ // 1. Validate every destination before any write so a symlinked kit cannot
20
165
  // leave a partially initialized target behind.
21
166
  for (const entry of entries) await assertSafePath(target, entry.relativePath);
167
+ for (const relativePath of POLICY_INIT_ARTIFACTS) await assertSafePath(target, relativePath);
22
168
 
169
+ // 4. Pre-scan every existing template destination during planning. A
170
+ // deterministic canonical kit conflict must fail before ANY initialization
171
+ // write: no adapter, no kit file, no policy artifact, no manifest.
172
+ const initPlan = new Map();
23
173
  for (const entry of entries) {
24
174
  const destination = ensureWithin(target, entry.relativePath);
25
- if (await fileExists(destination)) {
26
- actions.push({ action: "skip", path: entry.relativePath, reason: "exists" });
27
- if (entry.sourcePath === "PROJECT_PROFILE.md") {
175
+ if (!(await fileExists(destination))) continue;
176
+ const existingBytes = await readBytes(destination);
177
+ const classification = classifyExistingInitTemplate({ entry, existingBytes });
178
+ if (classification.kind === "KIT_CONFLICT") {
179
+ throw kitConflictError(entry.relativePath);
180
+ }
181
+ initPlan.set(entry.relativePath, { ...classification, existingBytes });
182
+ }
183
+
184
+ // 5. Compute the full executable-policy bootstrap in memory first. Unknown
185
+ // or low-confidence discovery is legitimate autonomy and succeeds; only
186
+ // genuine discovery/serialization/computation failures fail closed.
187
+ const { discoverPolicy } = await import("../core/policy-discovery.js");
188
+ const {
189
+ loadEffectiveRules,
190
+ computePolicyLockData,
191
+ writeDiscoveryReport,
192
+ writePolicyLock,
193
+ detectPolicyCapability,
194
+ verifyPolicyLock,
195
+ readDiscoveryReport,
196
+ readPolicyLock,
197
+ readBaseline,
198
+ writeBaseline,
199
+ } = await import("../core/policy-engine.js");
200
+ const { createBaselineFromViolations } = await import("../core/policy-baseline.js");
201
+
202
+ let discovery;
203
+ let baseline;
204
+ let effectiveRules;
205
+ let lock;
206
+ try {
207
+ discovery = await discoverPolicy({ target });
208
+ if (hooks.afterPolicyDiscovery) {
209
+ discovery = await hooks.afterPolicyDiscovery(discovery, { target, packageRoot });
210
+ }
211
+ baseline = createBaselineFromViolations([]);
212
+ effectiveRules = await loadEffectiveRules(target, packageRoot);
213
+ lock = computePolicyLockData(effectiveRules, baseline);
214
+ } catch (cause) {
215
+ throw policyInitializationError(cause);
216
+ }
217
+
218
+ // 6. Pre-reconcile existing policy artifacts during planning (read-only) for
219
+ // BOTH real init and dry-run, so a deterministic policy conflict is detected
220
+ // with identical semantics and before any mutation. dry-run stays zero-write
221
+ // and only reports the resulting plan.
222
+ const discoveryReconcile = await reconcilePolicyArtifact(
223
+ target,
224
+ packageRoot,
225
+ readDiscoveryReport,
226
+ discovery,
227
+ { semanticEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b), label: `${PROJECT_ARTIFACT_PATHS.policyDiscovery}` },
228
+ );
229
+ const baselineReconcile = await reconcilePolicyArtifact(
230
+ target,
231
+ packageRoot,
232
+ readBaseline,
233
+ baseline,
234
+ { semanticEqual: baselineSemanticallyEqual, label: `${PROJECT_ARTIFACT_PATHS.policyBaseline}` },
235
+ );
236
+ const lockReconcile = await reconcilePolicyArtifact(
237
+ target,
238
+ packageRoot,
239
+ readPolicyLock,
240
+ lock,
241
+ { semanticEqual: lockSemanticallyEqual, label: `${PROJECT_ARTIFACT_PATHS.policyLock}` },
242
+ );
243
+
244
+ // 7. Mutate: write kit files per the precomputed plan. Existing files that
245
+ // match the shipped template are resumable init output; PROJECT_PROFILE.md
246
+ // is preserved by contract; canonical hidden kit conflicts were already
247
+ // rejected in planning; other pre-existing (brownfield) files are preserved
248
+ // as unowned.
249
+ for (const entry of entries) {
250
+ const destination = ensureWithin(target, entry.relativePath);
251
+ const planned = initPlan.get(entry.relativePath);
252
+ if (planned) {
253
+ if (planned.kind === "MATCH") {
254
+ actions.push({ action: "reuse", path: entry.relativePath, reason: "matches template" });
255
+ } else if (planned.kind === "PRESERVE_PROFILE") {
256
+ actions.push({ action: "skip", path: entry.relativePath, reason: "preserved" });
28
257
  manifest.files[entry.relativePath] = {
29
- sha256: sha256(await readBytes(destination)),
258
+ sha256: planned.existingHash,
30
259
  preserve: true,
31
260
  };
261
+ continue;
262
+ } else {
263
+ actions.push({ action: "skip", path: entry.relativePath, reason: "pre-existing" });
264
+ continue;
32
265
  }
266
+ manifest.files[entry.relativePath] = {
267
+ sha256: planned.templateHash,
268
+ preserve: entry.sourcePath === "PROJECT_PROFILE.md",
269
+ };
33
270
  continue;
34
271
  }
35
272
 
@@ -44,6 +281,67 @@ export async function runInit({ target, dryRun, packageRoot, packageVersion }) {
44
281
  };
45
282
  }
46
283
 
284
+ // 8-9. Write/reuse policy artifacts and verify before committing authority.
285
+ // dry-run performs no mutation but reports the accurate plan (reuse vs
286
+ // would-write) computed by the read-only reconciliation above.
287
+ if (dryRun) {
288
+ actions.push(
289
+ { action: discoveryReconcile.action === "reuse" ? "reuse" : "would-write", path: PROJECT_ARTIFACT_PATHS.policyDiscovery, reason: discoveryReconcile.action === "reuse" ? "matches canonical state" : "dry-run" },
290
+ { action: baselineReconcile.action === "reuse" ? "reuse" : "would-write", path: PROJECT_ARTIFACT_PATHS.policyBaseline, reason: baselineReconcile.action === "reuse" ? "matches canonical state" : "dry-run" },
291
+ { action: lockReconcile.action === "reuse" ? "reuse" : "would-write", path: PROJECT_ARTIFACT_PATHS.policyLock, reason: lockReconcile.action === "reuse" ? "matches canonical state" : "dry-run" },
292
+ );
293
+ } else {
294
+ actions.push(
295
+ { action: discoveryReconcile.action === "reuse" ? "reuse" : "created", path: PROJECT_ARTIFACT_PATHS.policyDiscovery, reason: discoveryReconcile.action === "reuse" ? "matches canonical state" : "initialized" },
296
+ { action: baselineReconcile.action === "reuse" ? "reuse" : "created", path: PROJECT_ARTIFACT_PATHS.policyBaseline, reason: baselineReconcile.action === "reuse" ? "matches canonical state" : "initialized" },
297
+ { action: lockReconcile.action === "reuse" ? "reuse" : "created", path: PROJECT_ARTIFACT_PATHS.policyLock, reason: lockReconcile.action === "reuse" ? "matches canonical state" : "initialized" },
298
+ );
299
+
300
+ try {
301
+ if (discoveryReconcile.action === "write") {
302
+ await writeDiscoveryReport(target, discovery, packageRoot);
303
+ }
304
+ if (hooks.beforeBaselineWrite) {
305
+ await hooks.beforeBaselineWrite({ target, packageRoot, baseline });
306
+ }
307
+ if (baselineReconcile.action === "write") {
308
+ await writeBaseline(target, baseline, packageRoot);
309
+ }
310
+ if (hooks.beforePolicyLockWrite) {
311
+ await hooks.beforePolicyLockWrite({ target, packageRoot, lock });
312
+ }
313
+ if (lockReconcile.action === "write") {
314
+ await writePolicyLock(target, lock, packageRoot);
315
+ }
316
+ } catch (cause) {
317
+ throw policyInitializationError(cause);
318
+ }
319
+
320
+ // 9. Verify canonical policy state before committing authority.
321
+ try {
322
+ const capability = await detectPolicyCapability(target, packageRoot);
323
+ if (capability !== "AVAILABLE") {
324
+ throw new Error(`Executable policy capability is ${capability} after bootstrap`);
325
+ }
326
+ const lockVerification = await verifyPolicyLock(target, packageRoot);
327
+ if (lockVerification.status !== "VALID") {
328
+ throw new Error(`Executable policy lock verification failed: ${lockVerification.status}`);
329
+ }
330
+ } catch (cause) {
331
+ throw policyInitializationError(cause);
332
+ }
333
+ }
334
+
335
+ // 10. Commit manifest authority LAST: successful initialization is only
336
+ // real once the manifest exists.
337
+ if (hooks.beforeManifestWrite) {
338
+ try {
339
+ await hooks.beforeManifestWrite({ target, packageRoot, manifest, actions });
340
+ } catch (cause) {
341
+ throw policyInitializationError(cause);
342
+ }
343
+ }
47
344
  await writeManifest(target, manifest, { dryRun });
345
+
48
346
  return { actions, manifest };
49
347
  }
@@ -0,0 +1,51 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { diffPolicies } from "../core/policy-diff.js";
4
+ import {
5
+ loadEffectiveRules,
6
+ readBaseline,
7
+ readTaskPolicySnapshot,
8
+ } from "../core/policy-engine.js";
9
+
10
+ export async function runPolicyDiff({
11
+ target = process.cwd(),
12
+ packageRoot,
13
+ taskId = null,
14
+ before = null,
15
+ after = null,
16
+ } = {}) {
17
+ let beforePolicy;
18
+ let afterPolicy;
19
+
20
+ if (before) {
21
+ const rawBefore = await readFile(path.resolve(target, before), "utf8");
22
+ beforePolicy = JSON.parse(rawBefore);
23
+ } else if (taskId) {
24
+ const snapshot = await readTaskPolicySnapshot(target, taskId, packageRoot);
25
+ beforePolicy = snapshot ? { rules: snapshot.rules, baseline: { entries: [] } } : { rules: [], baseline: { entries: [] } };
26
+ } else {
27
+ beforePolicy = { rules: [], baseline: { entries: [] } };
28
+ }
29
+
30
+ if (after) {
31
+ const rawAfter = await readFile(path.resolve(target, after), "utf8");
32
+ afterPolicy = JSON.parse(rawAfter);
33
+ } else {
34
+ const rules = await loadEffectiveRules(target, packageRoot);
35
+ const baseline = await readBaseline(target, packageRoot);
36
+ afterPolicy = { rules, baseline };
37
+ }
38
+
39
+ return diffPolicies(beforePolicy, afterPolicy);
40
+ }
41
+
42
+ export function formatPolicyDiffResult(result) {
43
+ const lines = [
44
+ `FORGELOOP POLICY DIFF: ${result.classification}`,
45
+ `Changes: ${result.changes?.length ?? 0}`,
46
+ ];
47
+ for (const c of result.changes ?? []) {
48
+ lines.push(` - [${c.type}] ${c.path}: ${c.description}`);
49
+ }
50
+ return `${lines.join("\n")}\n`;
51
+ }
@@ -0,0 +1,42 @@
1
+ import { discoverPolicy } from "../core/policy-discovery.js";
2
+ import {
3
+ computePolicyLockData,
4
+ loadEffectiveRules,
5
+ readBaseline,
6
+ writeDiscoveryReport,
7
+ writePolicyLock,
8
+ } from "../core/policy-engine.js";
9
+
10
+ export async function runPolicyDiscover({ target = process.cwd(), packageRoot, write = false } = {}) {
11
+ const discovery = await discoverPolicy({ target });
12
+ let lock = null;
13
+
14
+ if (write) {
15
+ await writeDiscoveryReport(target, discovery, packageRoot);
16
+ const baseline = await readBaseline(target, packageRoot);
17
+ const rules = await loadEffectiveRules(target, packageRoot);
18
+ lock = computePolicyLockData(rules, baseline);
19
+ await writePolicyLock(target, lock, packageRoot);
20
+ }
21
+
22
+ return {
23
+ ...discovery,
24
+ written: Boolean(write),
25
+ lock: lock?.digest ?? null,
26
+ };
27
+ }
28
+
29
+ export function formatPolicyDiscoverResult(result) {
30
+ const lines = [
31
+ "FORGELOOP POLICY DISCOVERY:",
32
+ `Languages: ${result.languages?.join(", ") || "none detected"}`,
33
+ `Testing: ${result.testing?.detected ? `detected (${result.testing.framework || result.testing.command?.join(" ")}) [${result.testing.confidence}]` : "none"}`,
34
+ `Linting: ${result.linting?.detected ? `detected (${result.linting.tool || result.linting.command?.join(" ")}) [${result.linting.confidence}]` : "none"}`,
35
+ `Architecture: ${result.architecture?.value ? `${result.architecture.value} [${result.architecture.confidence}]` : "none [UNKNOWN]"}`,
36
+ `Discovered Rules: ${result.discoveredRules?.length ?? 0}`,
37
+ ];
38
+ for (const rule of result.discoveredRules ?? []) {
39
+ lines.push(` - ${rule.id} (${rule.severity}${rule.blocking ? ", BLOCKING" : ", ADVISORY"}): ${rule.why}`);
40
+ }
41
+ return `${lines.join("\n")}\n`;
42
+ }