qualia-framework 6.2.9 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/AGENTS.md +1 -0
  2. package/CLAUDE.md +1 -0
  3. package/README.md +26 -30
  4. package/agents/builder.md +7 -7
  5. package/agents/planner.md +39 -3
  6. package/agents/research-synthesizer.md +1 -1
  7. package/agents/researcher.md +3 -3
  8. package/agents/roadmapper.md +7 -7
  9. package/agents/verifier.md +18 -6
  10. package/agents/visual-evaluator.md +8 -7
  11. package/bin/cli.js +160 -16
  12. package/bin/command-surface.js +71 -0
  13. package/bin/contract-runner.js +219 -0
  14. package/bin/harness-eval.js +296 -0
  15. package/bin/host-adapters.js +66 -0
  16. package/bin/install.js +116 -172
  17. package/bin/knowledge-flush.js +21 -10
  18. package/bin/knowledge.js +1 -1
  19. package/bin/plan-contract.js +99 -2
  20. package/bin/planning-hygiene.js +262 -0
  21. package/bin/project-snapshot.js +20 -0
  22. package/bin/report-payload.js +18 -0
  23. package/bin/runtime-manifest.js +35 -0
  24. package/bin/state-ledger.js +184 -0
  25. package/bin/state.js +330 -20
  26. package/bin/trust-score.js +268 -0
  27. package/bin/work-packet.js +228 -0
  28. package/docs/erp-contract.md +81 -1
  29. package/docs/onboarding.html +4 -14
  30. package/guide.md +16 -16
  31. package/hooks/fawzi-approval-guard.js +143 -0
  32. package/hooks/pre-deploy-gate.js +74 -1
  33. package/hooks/session-start.js +29 -1
  34. package/package.json +1 -1
  35. package/qualia-design/design-rubric.md +17 -5
  36. package/qualia-design/frontend.md +6 -2
  37. package/qualia-design/graphics.md +47 -0
  38. package/rules/codex-goal.md +1 -1
  39. package/rules/command-output.md +35 -0
  40. package/rules/one-opinion.md +2 -2
  41. package/rules/speed.md +0 -1
  42. package/skills/qualia/SKILL.md +12 -12
  43. package/skills/qualia-build/SKILL.md +20 -14
  44. package/skills/qualia-discuss/SKILL.md +10 -10
  45. package/skills/qualia-doctor/SKILL.md +140 -0
  46. package/skills/qualia-feature/SKILL.md +24 -22
  47. package/skills/qualia-fix/SKILL.md +216 -0
  48. package/skills/qualia-handoff/SKILL.md +9 -9
  49. package/skills/qualia-learn/SKILL.md +11 -11
  50. package/skills/qualia-map/SKILL.md +2 -2
  51. package/skills/qualia-milestone/SKILL.md +15 -15
  52. package/skills/qualia-new/REFERENCE.md +9 -9
  53. package/skills/qualia-new/SKILL.md +14 -14
  54. package/skills/qualia-optimize/REFERENCE.md +1 -1
  55. package/skills/qualia-optimize/SKILL.md +23 -16
  56. package/skills/qualia-plan/SKILL.md +23 -13
  57. package/skills/qualia-polish/REFERENCE.md +15 -15
  58. package/skills/qualia-polish/SKILL.md +81 -21
  59. package/skills/qualia-polish/scripts/loop.mjs +3 -3
  60. package/skills/qualia-polish/scripts/score.mjs +9 -3
  61. package/skills/{qualia-vibe/scripts/extract.mjs → qualia-polish/scripts/vibe-extract.mjs} +5 -5
  62. package/skills/{qualia-vibe/scripts/tokens.mjs → qualia-polish/scripts/vibe-tokens.mjs} +6 -6
  63. package/skills/qualia-postmortem/SKILL.md +9 -9
  64. package/skills/qualia-report/SKILL.md +23 -23
  65. package/skills/qualia-research/SKILL.md +5 -5
  66. package/skills/qualia-review/SKILL.md +28 -12
  67. package/skills/qualia-road/SKILL.md +30 -22
  68. package/skills/qualia-ship/SKILL.md +31 -24
  69. package/skills/qualia-test/SKILL.md +5 -5
  70. package/skills/qualia-verify/SKILL.md +45 -23
  71. package/skills/zoho-workflow/SKILL.md +1 -1
  72. package/templates/help.html +11 -20
  73. package/tests/bin.test.sh +178 -76
  74. package/tests/hooks.test.sh +81 -1
  75. package/tests/install-smoke.test.sh +35 -5
  76. package/tests/lib.test.sh +432 -0
  77. package/tests/published-install-smoke.test.sh +4 -3
  78. package/tests/refs.test.sh +9 -4
  79. package/tests/runner.js +32 -28
  80. package/tests/skills.test.sh +4 -4
  81. package/tests/state.test.sh +133 -3
  82. package/skills/qualia-debug/SKILL.md +0 -185
  83. package/skills/qualia-flush/SKILL.md +0 -198
  84. package/skills/qualia-help/SKILL.md +0 -74
  85. package/skills/qualia-hook-gen/SKILL.md +0 -206
  86. package/skills/qualia-idk/SKILL.md +0 -166
  87. package/skills/qualia-issues/SKILL.md +0 -151
  88. package/skills/qualia-pause/SKILL.md +0 -68
  89. package/skills/qualia-resume/SKILL.md +0 -52
  90. package/skills/qualia-skill-new/SKILL.md +0 -173
  91. package/skills/qualia-triage/SKILL.md +0 -152
  92. package/skills/qualia-vibe/SKILL.md +0 -226
  93. package/skills/qualia-zoom/SKILL.md +0 -51
package/bin/state.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  const fs = require("fs");
6
6
  const path = require("path");
7
+ const stateLedger = require("./state-ledger.js");
7
8
 
8
9
  const PLANNING = ".planning";
9
10
  const STATE_FILE = path.join(PLANNING, "STATE.md");
@@ -191,6 +192,22 @@ function readTracking() {
191
192
  }
192
193
  }
193
194
 
195
+ function readTrackingRaw() {
196
+ try {
197
+ return fs.readFileSync(TRACKING_FILE, "utf8");
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+
203
+ function parseTrackingRaw(raw) {
204
+ try {
205
+ return raw ? JSON.parse(raw) : null;
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+
194
211
  function writeTracking(t) {
195
212
  atomicWrite(TRACKING_FILE, JSON.stringify(t, null, 2) + "\n");
196
213
  }
@@ -451,6 +468,14 @@ function checkPreconditions(current, target, opts) {
451
468
  return fail("MISSING_FILE", `Verification file not found: ${vFile}`);
452
469
  if (!opts.verification || !["pass", "fail"].includes(opts.verification))
453
470
  return fail("MISSING_ARG", "--verification must be 'pass' or 'fail'");
471
+ if (opts.verification === "pass") {
472
+ const vContent = fs.readFileSync(vFile, "utf8");
473
+ if (/\bINSUFFICIENT EVIDENCE\b/.test(vContent)) {
474
+ return fail("INSUFFICIENT_EVIDENCE", `${vFile} contains INSUFFICIENT EVIDENCE; PASS is not allowed`);
475
+ }
476
+ const evidenceCheck = checkMachineEvidence(phase);
477
+ if (!evidenceCheck.ok) return evidenceCheck;
478
+ }
454
479
  }
455
480
 
456
481
  if (target === "shipped") {
@@ -484,6 +509,41 @@ function fail(error, message) {
484
509
  return { ok: false, error, message };
485
510
  }
486
511
 
512
+ function checkMachineEvidence(phase) {
513
+ const contractFile = path.join(PLANNING, `phase-${phase}-contract.json`);
514
+ if (!fs.existsSync(contractFile)) return { ok: true };
515
+
516
+ const evidenceFile = path.join(PLANNING, "evidence", `phase-${phase}-contract-run.json`);
517
+ if (!fs.existsSync(evidenceFile)) {
518
+ return fail(
519
+ "MISSING_EVIDENCE",
520
+ `Contract exists for phase ${phase}, but machine evidence is missing: ${evidenceFile}. Run contract-runner.js or qualia-framework eval --run --write.`
521
+ );
522
+ }
523
+ let evidence;
524
+ try {
525
+ evidence = JSON.parse(fs.readFileSync(evidenceFile, "utf8"));
526
+ } catch (e) {
527
+ return fail("INVALID_EVIDENCE", `Could not parse ${evidenceFile}: ${e.message}`);
528
+ }
529
+ if (!evidence || evidence.ok !== true) {
530
+ return fail("FAILING_EVIDENCE", `${evidenceFile} does not prove the contract passed`);
531
+ }
532
+ return { ok: true };
533
+ }
534
+
535
+ function recordLedgerEvent(meta) {
536
+ try {
537
+ return stateLedger.append(process.cwd(), {
538
+ command: `state.js ${process.argv.slice(2).join(" ")}`.trim(),
539
+ ...meta,
540
+ });
541
+ } catch (err) {
542
+ try { _trace("state-ledger", "error", { action: meta && meta.action, error: err.message }); } catch {}
543
+ return { ok: false, error: err.message };
544
+ }
545
+ }
546
+
487
547
  // ─── Next Command Logic ──────────────────────────────────
488
548
  function nextCommand(status, phase, totalPhases, verification) {
489
549
  switch (status) {
@@ -687,8 +747,10 @@ function cmdTransition(opts) {
687
747
  const target = opts.to;
688
748
  if (!target) return output(fail("MISSING_ARG", "--to is required"));
689
749
 
690
- const t = readTracking();
691
- const s = parseStateMd(readState());
750
+ const beforeStateRaw = readState();
751
+ const beforeTrackingRaw = readTrackingRaw();
752
+ const t = parseTrackingRaw(beforeTrackingRaw);
753
+ const s = parseStateMd(beforeStateRaw);
692
754
  if (!t || !s) {
693
755
  return output(fail("NO_PROJECT", "No .planning/ found. Run /qualia-new."));
694
756
  }
@@ -705,7 +767,31 @@ function cmdTransition(opts) {
705
767
 
706
768
  // Note/activity short-circuit (no status change, no precondition check)
707
769
  const noteResult = applyNoteOrActivity(target, s, t, opts);
708
- if (noteResult) return output(noteResult);
770
+ if (noteResult) {
771
+ const ledger = recordLedgerEvent({
772
+ action: target,
773
+ phase_before: s.phase,
774
+ phase_after: s.phase,
775
+ status_before: s.status,
776
+ status_after: s.status,
777
+ state_before: s,
778
+ state_after: s,
779
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
780
+ tracking_after: t,
781
+ state_raw_before: beforeStateRaw,
782
+ state_raw_after: readState(),
783
+ tracking_raw_before: beforeTrackingRaw,
784
+ tracking_raw_after: readTrackingRaw(),
785
+ evidence_refs: opts.evidence ? [opts.evidence] : [],
786
+ });
787
+ if (ledger.ok) {
788
+ noteResult.ledger_event_id = ledger.event_id;
789
+ noteResult.ledger_event_hash = ledger.event_hash;
790
+ } else {
791
+ noteResult.ledger_error = ledger.error;
792
+ }
793
+ return output(noteResult);
794
+ }
709
795
 
710
796
  const phase = parseInt(opts.phase) || s.phase;
711
797
 
@@ -746,6 +832,23 @@ function cmdTransition(opts) {
746
832
  const writeError = commitTransitionAtomic(s, t);
747
833
  if (writeError) return output(writeError);
748
834
 
835
+ const ledger = recordLedgerEvent({
836
+ action: target,
837
+ phase_before: phase,
838
+ phase_after: s.phase,
839
+ status_before: prevStatus,
840
+ status_after: s.status,
841
+ state_before: parseStateMd(beforeStateRaw),
842
+ state_after: s,
843
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
844
+ tracking_after: t,
845
+ state_raw_before: beforeStateRaw,
846
+ state_raw_after: readState(),
847
+ tracking_raw_before: beforeTrackingRaw,
848
+ tracking_raw_after: readTrackingRaw(),
849
+ evidence_refs: opts.evidence ? [opts.evidence] : [],
850
+ });
851
+
749
852
  // Trace transition for analytics
750
853
  _trace("state-transition", "allow", {
751
854
  phase: s.phase,
@@ -755,7 +858,7 @@ function cmdTransition(opts) {
755
858
  gap_closure: prevStatus === "verified" && target === "planned",
756
859
  });
757
860
 
758
- output({
861
+ const result = {
759
862
  ok: true,
760
863
  phase: s.phase,
761
864
  phase_name: s.phase_name,
@@ -764,7 +867,14 @@ function cmdTransition(opts) {
764
867
  verification: t.verification,
765
868
  gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
766
869
  next_command: nextCommand(s.status, s.phase, s.total_phases, t.verification),
767
- });
870
+ };
871
+ if (ledger.ok) {
872
+ result.ledger_event_id = ledger.event_id;
873
+ result.ledger_event_hash = ledger.event_hash;
874
+ } else {
875
+ result.ledger_error = ledger.error;
876
+ }
877
+ output(result);
768
878
  }
769
879
 
770
880
  function cmdInit(opts) {
@@ -825,6 +935,8 @@ function cmdInit(opts) {
825
935
  const date = now.split("T")[0];
826
936
 
827
937
  // Read existing tracking for lifetime data preservation across milestone resets
938
+ const beforeStateRaw = readState();
939
+ const beforeTrackingRaw = readTrackingRaw();
828
940
  const prev = readTracking();
829
941
  const prevLife = prev ? ensureLifetime(prev) : null;
830
942
 
@@ -906,7 +1018,23 @@ function cmdInit(opts) {
906
1018
  writeStateMd(s);
907
1019
  writeTracking(t);
908
1020
 
909
- output({
1021
+ const ledger = recordLedgerEvent({
1022
+ action: "init",
1023
+ phase_before: null,
1024
+ phase_after: 1,
1025
+ status_before: null,
1026
+ status_after: "setup",
1027
+ state_before: parseStateMd(beforeStateRaw),
1028
+ state_after: s,
1029
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1030
+ tracking_after: t,
1031
+ state_raw_before: beforeStateRaw,
1032
+ state_raw_after: readState(),
1033
+ tracking_raw_before: beforeTrackingRaw,
1034
+ tracking_raw_after: readTrackingRaw(),
1035
+ });
1036
+
1037
+ const result = {
910
1038
  ok: true,
911
1039
  action: "init",
912
1040
  project: opts.project,
@@ -914,10 +1042,19 @@ function cmdInit(opts) {
914
1042
  total_phases: totalPhases,
915
1043
  status: "setup",
916
1044
  next_command: "/qualia-plan 1",
917
- });
1045
+ };
1046
+ if (ledger.ok) {
1047
+ result.ledger_event_id = ledger.event_id;
1048
+ result.ledger_event_hash = ledger.event_hash;
1049
+ } else {
1050
+ result.ledger_error = ledger.error;
1051
+ }
1052
+ output(result);
918
1053
  }
919
1054
 
920
1055
  function cmdFix(opts) {
1056
+ const beforeStateRaw = readState();
1057
+ const beforeTrackingRaw = readTrackingRaw();
921
1058
  const raw = readState();
922
1059
  const t = readTracking();
923
1060
  if (!raw && !t) {
@@ -980,17 +1117,41 @@ function cmdFix(opts) {
980
1117
  return output(fail("WRITE_ERROR", e.message));
981
1118
  }
982
1119
 
983
- output({
1120
+ const ledger = recordLedgerEvent({
1121
+ action: "fix",
1122
+ phase_before: parsed.phase,
1123
+ phase_after: s.phase,
1124
+ status_before: parsed.status,
1125
+ status_after: s.status,
1126
+ state_before: parsed,
1127
+ state_after: s,
1128
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1129
+ tracking_after: t,
1130
+ state_raw_before: beforeStateRaw,
1131
+ state_raw_after: readState(),
1132
+ tracking_raw_before: beforeTrackingRaw,
1133
+ tracking_raw_after: readTrackingRaw(),
1134
+ });
1135
+
1136
+ const result = {
984
1137
  ok: true,
985
1138
  action: "fix",
986
1139
  previous_errors: previousErrors,
987
1140
  fixed: true,
988
- });
1141
+ };
1142
+ if (ledger.ok) {
1143
+ result.ledger_event_id = ledger.event_id;
1144
+ result.ledger_event_hash = ledger.event_hash;
1145
+ } else {
1146
+ result.ledger_error = ledger.error;
1147
+ }
1148
+ output(result);
989
1149
  }
990
1150
 
991
1151
  function cmdValidatePlan(opts) {
992
1152
  const phase = parseInt(opts.phase) || 1;
993
1153
  const planFile = path.join(PLANNING, `phase-${phase}-plan.md`);
1154
+ const contractFile = path.join(PLANNING, `phase-${phase}-contract.json`);
994
1155
 
995
1156
  if (!fs.existsSync(planFile)) {
996
1157
  return output(fail("MISSING_FILE", `Plan file not found: ${planFile}`));
@@ -1036,6 +1197,47 @@ function cmdValidatePlan(opts) {
1036
1197
  const warnings = [];
1037
1198
  const VALID_CHECK_TYPES = ["file-exists", "grep-match", "command-exit", "behavioral"];
1038
1199
  let contractCount = 0;
1200
+ let contractStatus = "missing";
1201
+ let contractErrors = [];
1202
+
1203
+ // JSON contracts are the new machine-readable trust layer. Keep missing
1204
+ // contracts as a warning by default for in-flight legacy projects; callers can
1205
+ // opt into the hard gate with --require-contract.
1206
+ try {
1207
+ const pc = require("./plan-contract.js");
1208
+ if (fs.existsSync(contractFile)) {
1209
+ const loaded = pc.readContractFile(contractFile);
1210
+ if (!loaded.ok) {
1211
+ contractStatus = "invalid";
1212
+ contractErrors = [loaded.message || loaded.error || "contract unreadable"];
1213
+ } else {
1214
+ contractErrors = pc.validate(loaded.contract);
1215
+ const drift = pc.checkDrift(contractFile, planFile);
1216
+ if (contractErrors.length > 0) {
1217
+ contractStatus = "invalid";
1218
+ } else if (drift.ok && drift.drift) {
1219
+ contractStatus = "drifted";
1220
+ contractErrors = [`source_plan_hash drift: stored ${drift.stored}, current ${drift.current}`];
1221
+ } else {
1222
+ contractStatus = "valid";
1223
+ contractCount = Array.isArray(loaded.contract.tasks) ? loaded.contract.tasks.length : 0;
1224
+ }
1225
+ }
1226
+ }
1227
+ } catch (e) {
1228
+ contractStatus = "unavailable";
1229
+ contractErrors = [e.message];
1230
+ }
1231
+
1232
+ if (contractStatus === "missing") {
1233
+ const msg = `JSON contract missing: ${contractFile}`;
1234
+ if (opts.require_contract) errors.push(msg);
1235
+ else warnings.push(msg);
1236
+ } else if (contractStatus !== "valid") {
1237
+ const msg = `JSON contract ${contractStatus}: ${contractErrors.join("; ")}`;
1238
+ if (opts.require_contract) errors.push(msg);
1239
+ else warnings.push(msg);
1240
+ }
1039
1241
 
1040
1242
  if (/^## Verification Contract/m.test(content)) {
1041
1243
  // Extract the contract section (from header to next ## or end of file)
@@ -1049,9 +1251,10 @@ function cmdValidatePlan(opts) {
1049
1251
  if (nextH2 !== -1) contractSection = contractSection.substring(0, nextH2);
1050
1252
  // Each contract starts with ### Contract for Task N
1051
1253
  const contractBlocks = contractSection.match(/^### Contract for Task \d+/gm);
1052
- contractCount = contractBlocks ? contractBlocks.length : 0;
1254
+ const markdownContractCount = contractBlocks ? contractBlocks.length : 0;
1255
+ if (contractStatus !== "valid") contractCount = markdownContractCount;
1053
1256
 
1054
- if (contractCount === 0) {
1257
+ if (markdownContractCount === 0) {
1055
1258
  warnings.push("Verification Contract section exists but contains no contract blocks (expected '### Contract for Task N')");
1056
1259
  } else {
1057
1260
  // Split into individual contract blocks for validation
@@ -1092,9 +1295,9 @@ function cmdValidatePlan(opts) {
1092
1295
  }
1093
1296
 
1094
1297
  // Warn if contract count < task count
1095
- if (taskCount > 0 && contractCount > 0 && contractCount < taskCount) {
1298
+ if (taskCount > 0 && markdownContractCount > 0 && markdownContractCount < taskCount) {
1096
1299
  warnings.push(
1097
- `Only ${contractCount} contract(s) for ${taskCount} task(s) — not all tasks have verification contracts`
1300
+ `Only ${markdownContractCount} Markdown contract(s) for ${taskCount} task(s) — not all tasks have verification contracts`
1098
1301
  );
1099
1302
  }
1100
1303
  }
@@ -1119,6 +1322,9 @@ function cmdValidatePlan(opts) {
1119
1322
  done_when_count: doneWhenCount,
1120
1323
  ac_count: acCount,
1121
1324
  contract_count: contractCount,
1325
+ contract_file: contractFile,
1326
+ contract_status: contractStatus,
1327
+ contract_errors: contractErrors.length > 0 ? contractErrors : undefined,
1122
1328
  warnings: warnings.length > 0 ? warnings : undefined,
1123
1329
  });
1124
1330
  }
@@ -1129,6 +1335,8 @@ function cmdValidatePlan(opts) {
1129
1335
  // hiccup) does NOT double-count. To re-close a milestone deliberately, pass
1130
1336
  // --force.
1131
1337
  function cmdCloseMilestone(opts) {
1338
+ const beforeStateRaw = readState();
1339
+ const beforeTrackingRaw = readTrackingRaw();
1132
1340
  const t = readTracking();
1133
1341
  const s = parseStateMd(readState());
1134
1342
  if (!t || !s) {
@@ -1252,19 +1460,44 @@ function cmdCloseMilestone(opts) {
1252
1460
  lifetime: t.lifetime,
1253
1461
  });
1254
1462
 
1255
- output({
1463
+ const ledger = recordLedgerEvent({
1464
+ action: "close-milestone",
1465
+ phase_before: s.phase,
1466
+ phase_after: s.phase,
1467
+ status_before: s.status,
1468
+ status_after: s.status,
1469
+ state_before: s,
1470
+ state_after: s,
1471
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1472
+ tracking_after: t,
1473
+ state_raw_before: beforeStateRaw,
1474
+ state_raw_after: readState(),
1475
+ tracking_raw_before: beforeTrackingRaw,
1476
+ tracking_raw_after: readTrackingRaw(),
1477
+ });
1478
+
1479
+ const result = {
1256
1480
  ok: true,
1257
1481
  action: "close-milestone",
1258
1482
  closed_milestone: closedMilestone,
1259
1483
  next_milestone: t.milestone,
1260
1484
  lifetime: t.lifetime,
1261
- });
1485
+ };
1486
+ if (ledger.ok) {
1487
+ result.ledger_event_id = ledger.event_id;
1488
+ result.ledger_event_hash = ledger.event_hash;
1489
+ } else {
1490
+ result.ledger_error = ledger.error;
1491
+ }
1492
+ output(result);
1262
1493
  }
1263
1494
 
1264
1495
  // ─── Backfill Lifetime ───────────────────────────────────
1265
1496
  // Reconstructs lifetime counters from STATE.md roadmap + plan files.
1266
1497
  // Safe to run multiple times (idempotent — recalculates from source).
1267
1498
  function cmdBackfillLifetime(opts) {
1499
+ const beforeStateRaw = readState();
1500
+ const beforeTrackingRaw = readTrackingRaw();
1268
1501
  const t = readTracking();
1269
1502
  const s = parseStateMd(readState());
1270
1503
  if (!t || !s) {
@@ -1327,7 +1560,23 @@ function cmdBackfillLifetime(opts) {
1327
1560
  phases_scanned: s.phases.length,
1328
1561
  });
1329
1562
 
1330
- output({
1563
+ const ledger = recordLedgerEvent({
1564
+ action: "backfill-lifetime",
1565
+ phase_before: s.phase,
1566
+ phase_after: s.phase,
1567
+ status_before: s.status,
1568
+ status_after: s.status,
1569
+ state_before: s,
1570
+ state_after: s,
1571
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1572
+ tracking_after: t,
1573
+ state_raw_before: beforeStateRaw,
1574
+ state_raw_after: readState(),
1575
+ tracking_raw_before: beforeTrackingRaw,
1576
+ tracking_raw_after: readTrackingRaw(),
1577
+ });
1578
+
1579
+ const result = {
1331
1580
  ok: true,
1332
1581
  action: "backfill-lifetime",
1333
1582
  previous,
@@ -1335,7 +1584,14 @@ function cmdBackfillLifetime(opts) {
1335
1584
  phases_scanned: s.phases.length,
1336
1585
  phases_completed: phasesCompleted,
1337
1586
  tasks_completed: tasksCompleted,
1338
- });
1587
+ };
1588
+ if (ledger.ok) {
1589
+ result.ledger_event_id = ledger.event_id;
1590
+ result.ledger_event_hash = ledger.event_hash;
1591
+ } else {
1592
+ result.ledger_error = ledger.error;
1593
+ }
1594
+ output(result);
1339
1595
  }
1340
1596
 
1341
1597
  // ─── Backfill Milestones from JOURNEY.md ─────────────────
@@ -1353,6 +1609,8 @@ function cmdBackfillLifetime(opts) {
1353
1609
  // Phase counting handles ranges (`1–13` → 13), comma lists (`14, 15, 16.1–16.6` → 8),
1354
1610
  // and "rolling" / "—" / "-" → 0.
1355
1611
  function cmdBackfillMilestones(opts) {
1612
+ const beforeStateRaw = readState();
1613
+ const beforeTrackingRaw = readTrackingRaw();
1356
1614
  const t = readTracking();
1357
1615
  if (!t) return output(fail("NO_PROJECT", "No .planning/ found."));
1358
1616
  ensureLifetime(t);
@@ -1507,7 +1765,24 @@ function cmdBackfillMilestones(opts) {
1507
1765
  lifetime: t.lifetime,
1508
1766
  });
1509
1767
 
1510
- output({
1768
+ const afterState = parseStateMd(readState());
1769
+ const ledger = recordLedgerEvent({
1770
+ action: "backfill-milestones",
1771
+ phase_before: afterState ? afterState.phase : undefined,
1772
+ phase_after: afterState ? afterState.phase : undefined,
1773
+ status_before: afterState ? afterState.status : undefined,
1774
+ status_after: afterState ? afterState.status : undefined,
1775
+ state_before: parseStateMd(beforeStateRaw),
1776
+ state_after: afterState,
1777
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1778
+ tracking_after: t,
1779
+ state_raw_before: beforeStateRaw,
1780
+ state_raw_after: readState(),
1781
+ tracking_raw_before: beforeTrackingRaw,
1782
+ tracking_raw_after: readTrackingRaw(),
1783
+ });
1784
+
1785
+ const result = {
1511
1786
  ok: true,
1512
1787
  action: "backfill-milestones",
1513
1788
  added,
@@ -1515,7 +1790,14 @@ function cmdBackfillMilestones(opts) {
1515
1790
  closed: closedSummaries,
1516
1791
  open_milestone: openRow ? { num: openRow.num, name: openRow.name } : null,
1517
1792
  lifetime: t.lifetime,
1518
- });
1793
+ };
1794
+ if (ledger.ok) {
1795
+ result.ledger_event_id = ledger.event_id;
1796
+ result.ledger_event_hash = ledger.event_hash;
1797
+ } else {
1798
+ result.ledger_error = ledger.error;
1799
+ }
1800
+ output(result);
1519
1801
  }
1520
1802
 
1521
1803
  // ─── Next Report ID ──────────────────────────────────────
@@ -1525,6 +1807,8 @@ function cmdBackfillMilestones(opts) {
1525
1807
  // to the ERP. If --peek is passed, the next id is returned WITHOUT
1526
1808
  // incrementing — useful for --dry-run previews.
1527
1809
  function cmdNextReportId(opts) {
1810
+ const beforeStateRaw = readState();
1811
+ const beforeTrackingRaw = readTrackingRaw();
1528
1812
  const t = readTracking();
1529
1813
  if (!t) return output(fail("NO_PROJECT", "No .planning/ found."));
1530
1814
  ensureLifetime(t);
@@ -1536,7 +1820,33 @@ function cmdNextReportId(opts) {
1536
1820
  t.last_updated = new Date().toISOString();
1537
1821
  writeTracking(t);
1538
1822
  }
1539
- output({ ok: true, action: "next-report-id", report_id: id, report_seq: next, peeked: peek });
1823
+ const result = { ok: true, action: "next-report-id", report_id: id, report_seq: next, peeked: peek };
1824
+ if (!peek) {
1825
+ const afterState = parseStateMd(readState());
1826
+ const beforeState = parseStateMd(beforeStateRaw);
1827
+ const ledger = recordLedgerEvent({
1828
+ action: "next-report-id",
1829
+ phase_before: beforeState ? beforeState.phase : undefined,
1830
+ phase_after: afterState ? afterState.phase : undefined,
1831
+ status_before: beforeState ? beforeState.status : undefined,
1832
+ status_after: afterState ? afterState.status : undefined,
1833
+ state_before: beforeState,
1834
+ state_after: afterState,
1835
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1836
+ tracking_after: t,
1837
+ state_raw_before: beforeStateRaw,
1838
+ state_raw_after: readState(),
1839
+ tracking_raw_before: beforeTrackingRaw,
1840
+ tracking_raw_after: readTrackingRaw(),
1841
+ });
1842
+ if (ledger.ok) {
1843
+ result.ledger_event_id = ledger.event_id;
1844
+ result.ledger_event_hash = ledger.event_hash;
1845
+ } else {
1846
+ result.ledger_error = ledger.error;
1847
+ }
1848
+ }
1849
+ output(result);
1540
1850
  }
1541
1851
 
1542
1852
  // ─── Output ──────────────────────────────────────────────