@cassiomc1/forgeloop 1.1.1 → 1.2.2

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 (79) 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 +124 -0
  7. package/ENG/premium-sites-studio-eng.md +28 -0
  8. package/ENG/taste-frontend-eng.md +3 -2
  9. package/ENG/test-code-eng.md +45 -0
  10. package/LOOP_ENGINEERING.md +74 -0
  11. package/LOOP_SYSTEM_DESIGN.md +9 -5
  12. package/ORCHESTRATOR_INTEGRATION.md +41 -6
  13. package/PROTOCOL_INTEGRATION.md +13 -0
  14. package/README.md +40 -6
  15. package/TERMINOLOGY.md +10 -0
  16. package/THIRD_PARTY_NOTICES.md +58 -1
  17. package/THREAT_MODEL.md +12 -1
  18. package/docs/ARTIFACT_REFERENCE.md +152 -2
  19. package/docs/CLI_REFERENCE.md +346 -30
  20. package/docs/CROSS_HARNESS_CONTINUITY.md +1 -0
  21. package/docs/DOCUMENTATION_GUIDE.md +41 -4
  22. package/docs/GETTING_STARTED.md +39 -8
  23. package/docs/RECIPES.md +66 -7
  24. package/docs/TROUBLESHOOTING.md +279 -6
  25. package/package.json +1 -1
  26. package/schemas/policy-baseline.schema.json +26 -0
  27. package/schemas/policy-discovery.schema.json +45 -0
  28. package/schemas/policy-lock.schema.json +16 -0
  29. package/schemas/policy-rules.schema.json +48 -0
  30. package/schemas/policy-snapshot.schema.json +16 -0
  31. package/src/cli.js +102 -1
  32. package/src/commands/baseline.js +120 -0
  33. package/src/commands/init.js +304 -6
  34. package/src/commands/next.js +15 -1
  35. package/src/commands/policy-diff.js +51 -0
  36. package/src/commands/policy-discover.js +42 -0
  37. package/src/commands/policy-status.js +33 -0
  38. package/src/commands/profile-interview.js +50 -0
  39. package/src/commands/progress.js +51 -0
  40. package/src/commands/reconcile-closure.js +49 -0
  41. package/src/commands/record-decision-criterion.js +34 -0
  42. package/src/commands/record-diagnosis.js +49 -0
  43. package/src/commands/rule-verify.js +36 -0
  44. package/src/commands/validate-receipt.js +38 -3
  45. package/src/core/artifact-registry.js +60 -0
  46. package/src/core/audit.js +24 -0
  47. package/src/core/cli-command-definitions.js +163 -7
  48. package/src/core/cli-metadata.js +1 -1
  49. package/src/core/completion-artifacts.js +29 -3
  50. package/src/core/completion.js +101 -10
  51. package/src/core/diagnosis-model.js +214 -0
  52. package/src/core/diagnosis.js +171 -0
  53. package/src/core/error-codes.js +292 -0
  54. package/src/core/events.js +47 -1
  55. package/src/core/execution-prerequisites.js +38 -20
  56. package/src/core/execution.js +20 -3
  57. package/src/core/native-adapters.js +14 -4
  58. package/src/core/next-action-model.js +40 -5
  59. package/src/core/next-action.js +234 -91
  60. package/src/core/phase.js +29 -0
  61. package/src/core/policy-adapters.js +276 -0
  62. package/src/core/policy-baseline.js +144 -0
  63. package/src/core/policy-diff.js +133 -0
  64. package/src/core/policy-discovery.js +225 -0
  65. package/src/core/policy-engine.js +533 -0
  66. package/src/core/policy-mutation.js +139 -0
  67. package/src/core/preflight-consistency.js +23 -15
  68. package/src/core/preflight-model.js +10 -2
  69. package/src/core/preflight.js +65 -1
  70. package/src/core/progress.js +143 -0
  71. package/src/core/protocol.js +8 -0
  72. package/src/core/reconcile-closure.js +173 -0
  73. package/src/core/schema-validation.js +6 -0
  74. package/src/core/settlement-model.js +85 -0
  75. package/src/core/settlement.js +78 -0
  76. package/src/core/task-context.js +11 -0
  77. package/src/core/task-discovery.js +67 -1
  78. package/src/core/task-paths.js +9 -0
  79. package/src/core/templates.js +5 -0
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "forgeloop://schemas/policy-rules.schema.json",
4
+ "title": "ForgeLoop executable policy rules",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "rules"],
7
+ "properties": {
8
+ "schemaVersion": { "const": 1 },
9
+ "rules": {
10
+ "type": "array",
11
+ "items": {
12
+ "type": "object",
13
+ "required": ["id", "severity", "source", "blocking", "why", "fix", "check"],
14
+ "properties": {
15
+ "id": { "type": "string", "minLength": 1 },
16
+ "severity": { "enum": ["HIGH", "MEDIUM", "LOW", "INFO"] },
17
+ "source": { "enum": ["builtin", "discovered", "project"] },
18
+ "blocking": { "type": "boolean" },
19
+ "why": { "type": "string", "minLength": 1 },
20
+ "fix": { "type": "string", "minLength": 1 },
21
+ "confidence": { "enum": ["HIGH", "MEDIUM", "LOW", "UNKNOWN"] },
22
+ "scope": {
23
+ "type": "object",
24
+ "properties": {
25
+ "includes": { "type": "array", "items": { "type": "string" } },
26
+ "excludes": { "type": "array", "items": { "type": "string" } }
27
+ },
28
+ "additionalProperties": false
29
+ },
30
+ "check": {
31
+ "type": "object",
32
+ "required": ["type"],
33
+ "properties": {
34
+ "type": { "type": "string", "minLength": 1 },
35
+ "adapter": { "type": "string" },
36
+ "command": { "type": "array", "items": { "type": "string" } },
37
+ "threshold": { "type": "number" },
38
+ "parameters": { "type": "object" }
39
+ },
40
+ "additionalProperties": true
41
+ }
42
+ },
43
+ "additionalProperties": true
44
+ }
45
+ }
46
+ },
47
+ "additionalProperties": false
48
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "forgeloop://schemas/policy-snapshot.schema.json",
4
+ "title": "ForgeLoop task policy snapshot",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "policyDigest", "rules"],
7
+ "properties": {
8
+ "schemaVersion": { "const": 1 },
9
+ "policyDigest": { "type": "string", "minLength": 1 },
10
+ "rules": { "type": "array", "items": { "type": ["string", "object"] } },
11
+ "baseline": { "type": "object" },
12
+ "baselineDigest": { "type": "string" },
13
+ "capturedAt": { "type": "string" }
14
+ },
15
+ "additionalProperties": false
16
+ }
package/src/cli.js CHANGED
@@ -21,11 +21,21 @@ 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";
36
+ import { formatRecordDiagnosisResult, runRecordDiagnosis } from "./commands/record-diagnosis.js";
37
+ import { formatProgressResult, runProgress } from "./commands/progress.js";
38
+ import { formatRecordDecisionCriterionResult, runRecordDecisionCriterion } from "./commands/record-decision-criterion.js";
29
39
  import { formatNextActionResult, runNext } from "./commands/next.js";
30
40
  import { formatContinuityResult, runContinuity } from "./commands/continuity.js";
31
41
  import { formatRecordContinuityResult, runRecordContinuity } from "./commands/record-continuity.js";
@@ -336,6 +346,18 @@ export function validateCliSemantics({ command, options } = {}) {
336
346
  throw new Error("run-check requires -- followed by an exact command argv");
337
347
  }
338
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
+ }
339
361
  }
340
362
 
341
363
  export function parseArgs(argv) {
@@ -500,6 +522,36 @@ export const COMMAND_HANDLERS = Object.freeze({
500
522
  console.log(options.json ? JSON.stringify(result, null, 2) : formatRecordTerminalResult(result));
501
523
  return 0;
502
524
  },
525
+ "record-diagnosis": async ({ target, packageRoot, options }) => {
526
+ const result = await runRecordDiagnosis({
527
+ target,
528
+ packageRoot,
529
+ hypothesis: options.hypothesis,
530
+ failureClass: options.failureClass,
531
+ evidenceRefs: options.evidenceRefs,
532
+ settledBy: options.settledBy,
533
+ nextSafeAction: options.nextSafeAction,
534
+ taskId: options.task,
535
+ });
536
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatRecordDiagnosisResult(result));
537
+ return 0;
538
+ },
539
+ progress: async ({ target, packageRoot, options }) => {
540
+ const result = await runProgress({ target, packageRoot, taskId: options.task });
541
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatProgressResult(result));
542
+ return result.status === "STALLED" ? 1 : 0;
543
+ },
544
+ "record-decision-criterion": async ({ target, packageRoot, options }) => {
545
+ const result = await runRecordDecisionCriterion({
546
+ target,
547
+ packageRoot,
548
+ decision: options.decision,
549
+ settledBy: options.settledBy,
550
+ taskId: options.task,
551
+ });
552
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatRecordDecisionCriterionResult(result));
553
+ return 0;
554
+ },
503
555
  complete: async ({ target, packageRoot, options }) => {
504
556
  const result = await runComplete({ target, packageRoot, strict: options.strict, taskId: options.task });
505
557
  console.log(options.json ? JSON.stringify(result, null, 2) : formatCompleteResult(result));
@@ -520,6 +572,42 @@ export const COMMAND_HANDLERS = Object.freeze({
520
572
  console.log(options.json ? JSON.stringify(result, null, 2) : formatPolicyResult(result));
521
573
  return 0;
522
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
+ },
523
611
  bundle: async ({ target, packageRoot, options }) => {
524
612
  const result = await runBundle({ target, packageRoot, taskId: options.task });
525
613
  console.log(options.json ? JSON.stringify(result, null, 2) : formatBundleResult(result));
@@ -531,7 +619,7 @@ export const COMMAND_HANDLERS = Object.freeze({
531
619
  return result.ok ? 0 : 1;
532
620
  },
533
621
  "validate-receipt": async ({ target, packageRoot, options }) => {
534
- const result = await runValidateReceipt({ target, packageRoot, file: options.file });
622
+ const result = await runValidateReceipt({ target, packageRoot, file: options.file, taskId: options.task });
535
623
  console.log(options.json ? JSON.stringify(result, null, 2) : "valid: execution receipt");
536
624
  return 0;
537
625
  },
@@ -607,6 +695,19 @@ export const COMMAND_HANDLERS = Object.freeze({
607
695
  console.log(options.json ? JSON.stringify(result, null, 2) : formatTaskUnlockResult(result));
608
696
  return 0;
609
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
+ },
610
711
  update: async ({ target, packageRoot, packageVersion, options }) => {
611
712
  const result = await runUpdate({ target, dryRun: options.dryRun, packageRoot, packageVersion });
612
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
  }