qualia-framework 6.2.9 → 6.2.10

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 (72) hide show
  1. package/README.md +14 -11
  2. package/agents/builder.md +7 -7
  3. package/agents/planner.md +39 -3
  4. package/agents/research-synthesizer.md +1 -1
  5. package/agents/researcher.md +3 -3
  6. package/agents/roadmapper.md +7 -7
  7. package/agents/verifier.md +18 -6
  8. package/agents/visual-evaluator.md +8 -7
  9. package/bin/cli.js +111 -14
  10. package/bin/contract-runner.js +219 -0
  11. package/bin/host-adapters.js +66 -0
  12. package/bin/install.js +99 -152
  13. package/bin/plan-contract.js +99 -2
  14. package/bin/planning-hygiene.js +262 -0
  15. package/bin/runtime-manifest.js +32 -0
  16. package/bin/state-ledger.js +184 -0
  17. package/bin/state.js +299 -20
  18. package/bin/trust-score.js +276 -0
  19. package/docs/onboarding.html +5 -4
  20. package/guide.md +3 -2
  21. package/package.json +1 -1
  22. package/qualia-design/design-rubric.md +17 -5
  23. package/qualia-design/frontend.md +5 -1
  24. package/qualia-design/graphics.md +47 -0
  25. package/rules/command-output.md +35 -0
  26. package/skills/qualia/SKILL.md +10 -10
  27. package/skills/qualia-build/SKILL.md +20 -14
  28. package/skills/qualia-debug/SKILL.md +16 -8
  29. package/skills/qualia-discuss/SKILL.md +10 -10
  30. package/skills/qualia-doctor/SKILL.md +140 -0
  31. package/skills/qualia-feature/SKILL.md +23 -21
  32. package/skills/qualia-fix/SKILL.md +216 -0
  33. package/skills/qualia-flush/SKILL.md +9 -9
  34. package/skills/qualia-handoff/SKILL.md +9 -9
  35. package/skills/qualia-help/SKILL.md +3 -3
  36. package/skills/qualia-hook-gen/SKILL.md +1 -1
  37. package/skills/qualia-idk/SKILL.md +4 -4
  38. package/skills/qualia-issues/SKILL.md +2 -2
  39. package/skills/qualia-learn/SKILL.md +10 -10
  40. package/skills/qualia-map/SKILL.md +2 -2
  41. package/skills/qualia-milestone/SKILL.md +15 -15
  42. package/skills/qualia-new/REFERENCE.md +9 -9
  43. package/skills/qualia-new/SKILL.md +14 -14
  44. package/skills/qualia-optimize/REFERENCE.md +1 -1
  45. package/skills/qualia-optimize/SKILL.md +23 -16
  46. package/skills/qualia-pause/SKILL.md +2 -2
  47. package/skills/qualia-plan/SKILL.md +23 -13
  48. package/skills/qualia-polish/REFERENCE.md +14 -14
  49. package/skills/qualia-polish/SKILL.md +64 -19
  50. package/skills/qualia-polish/scripts/loop.mjs +3 -3
  51. package/skills/qualia-polish/scripts/score.mjs +9 -3
  52. package/skills/qualia-postmortem/SKILL.md +9 -9
  53. package/skills/qualia-report/SKILL.md +23 -23
  54. package/skills/qualia-research/SKILL.md +5 -5
  55. package/skills/qualia-resume/SKILL.md +4 -4
  56. package/skills/qualia-review/SKILL.md +28 -12
  57. package/skills/qualia-road/SKILL.md +18 -5
  58. package/skills/qualia-ship/SKILL.md +22 -22
  59. package/skills/qualia-skill-new/SKILL.md +13 -13
  60. package/skills/qualia-test/SKILL.md +5 -5
  61. package/skills/qualia-triage/SKILL.md +1 -1
  62. package/skills/qualia-verify/SKILL.md +37 -23
  63. package/skills/qualia-vibe/SKILL.md +13 -10
  64. package/skills/qualia-vibe/scripts/extract.mjs +1 -1
  65. package/skills/zoho-workflow/SKILL.md +1 -1
  66. package/templates/help.html +12 -10
  67. package/tests/bin.test.sh +34 -4
  68. package/tests/install-smoke.test.sh +22 -2
  69. package/tests/lib.test.sh +290 -0
  70. package/tests/runner.js +3 -0
  71. package/tests/skills.test.sh +4 -4
  72. package/tests/state.test.sh +65 -3
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
  }
@@ -484,6 +501,18 @@ function fail(error, message) {
484
501
  return { ok: false, error, message };
485
502
  }
486
503
 
504
+ function recordLedgerEvent(meta) {
505
+ try {
506
+ return stateLedger.append(process.cwd(), {
507
+ command: `state.js ${process.argv.slice(2).join(" ")}`.trim(),
508
+ ...meta,
509
+ });
510
+ } catch (err) {
511
+ try { _trace("state-ledger", "error", { action: meta && meta.action, error: err.message }); } catch {}
512
+ return { ok: false, error: err.message };
513
+ }
514
+ }
515
+
487
516
  // ─── Next Command Logic ──────────────────────────────────
488
517
  function nextCommand(status, phase, totalPhases, verification) {
489
518
  switch (status) {
@@ -687,8 +716,10 @@ function cmdTransition(opts) {
687
716
  const target = opts.to;
688
717
  if (!target) return output(fail("MISSING_ARG", "--to is required"));
689
718
 
690
- const t = readTracking();
691
- const s = parseStateMd(readState());
719
+ const beforeStateRaw = readState();
720
+ const beforeTrackingRaw = readTrackingRaw();
721
+ const t = parseTrackingRaw(beforeTrackingRaw);
722
+ const s = parseStateMd(beforeStateRaw);
692
723
  if (!t || !s) {
693
724
  return output(fail("NO_PROJECT", "No .planning/ found. Run /qualia-new."));
694
725
  }
@@ -705,7 +736,31 @@ function cmdTransition(opts) {
705
736
 
706
737
  // Note/activity short-circuit (no status change, no precondition check)
707
738
  const noteResult = applyNoteOrActivity(target, s, t, opts);
708
- if (noteResult) return output(noteResult);
739
+ if (noteResult) {
740
+ const ledger = recordLedgerEvent({
741
+ action: target,
742
+ phase_before: s.phase,
743
+ phase_after: s.phase,
744
+ status_before: s.status,
745
+ status_after: s.status,
746
+ state_before: s,
747
+ state_after: s,
748
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
749
+ tracking_after: t,
750
+ state_raw_before: beforeStateRaw,
751
+ state_raw_after: readState(),
752
+ tracking_raw_before: beforeTrackingRaw,
753
+ tracking_raw_after: readTrackingRaw(),
754
+ evidence_refs: opts.evidence ? [opts.evidence] : [],
755
+ });
756
+ if (ledger.ok) {
757
+ noteResult.ledger_event_id = ledger.event_id;
758
+ noteResult.ledger_event_hash = ledger.event_hash;
759
+ } else {
760
+ noteResult.ledger_error = ledger.error;
761
+ }
762
+ return output(noteResult);
763
+ }
709
764
 
710
765
  const phase = parseInt(opts.phase) || s.phase;
711
766
 
@@ -746,6 +801,23 @@ function cmdTransition(opts) {
746
801
  const writeError = commitTransitionAtomic(s, t);
747
802
  if (writeError) return output(writeError);
748
803
 
804
+ const ledger = recordLedgerEvent({
805
+ action: target,
806
+ phase_before: phase,
807
+ phase_after: s.phase,
808
+ status_before: prevStatus,
809
+ status_after: s.status,
810
+ state_before: parseStateMd(beforeStateRaw),
811
+ state_after: s,
812
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
813
+ tracking_after: t,
814
+ state_raw_before: beforeStateRaw,
815
+ state_raw_after: readState(),
816
+ tracking_raw_before: beforeTrackingRaw,
817
+ tracking_raw_after: readTrackingRaw(),
818
+ evidence_refs: opts.evidence ? [opts.evidence] : [],
819
+ });
820
+
749
821
  // Trace transition for analytics
750
822
  _trace("state-transition", "allow", {
751
823
  phase: s.phase,
@@ -755,7 +827,7 @@ function cmdTransition(opts) {
755
827
  gap_closure: prevStatus === "verified" && target === "planned",
756
828
  });
757
829
 
758
- output({
830
+ const result = {
759
831
  ok: true,
760
832
  phase: s.phase,
761
833
  phase_name: s.phase_name,
@@ -764,7 +836,14 @@ function cmdTransition(opts) {
764
836
  verification: t.verification,
765
837
  gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
766
838
  next_command: nextCommand(s.status, s.phase, s.total_phases, t.verification),
767
- });
839
+ };
840
+ if (ledger.ok) {
841
+ result.ledger_event_id = ledger.event_id;
842
+ result.ledger_event_hash = ledger.event_hash;
843
+ } else {
844
+ result.ledger_error = ledger.error;
845
+ }
846
+ output(result);
768
847
  }
769
848
 
770
849
  function cmdInit(opts) {
@@ -825,6 +904,8 @@ function cmdInit(opts) {
825
904
  const date = now.split("T")[0];
826
905
 
827
906
  // Read existing tracking for lifetime data preservation across milestone resets
907
+ const beforeStateRaw = readState();
908
+ const beforeTrackingRaw = readTrackingRaw();
828
909
  const prev = readTracking();
829
910
  const prevLife = prev ? ensureLifetime(prev) : null;
830
911
 
@@ -906,7 +987,23 @@ function cmdInit(opts) {
906
987
  writeStateMd(s);
907
988
  writeTracking(t);
908
989
 
909
- output({
990
+ const ledger = recordLedgerEvent({
991
+ action: "init",
992
+ phase_before: null,
993
+ phase_after: 1,
994
+ status_before: null,
995
+ status_after: "setup",
996
+ state_before: parseStateMd(beforeStateRaw),
997
+ state_after: s,
998
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
999
+ tracking_after: t,
1000
+ state_raw_before: beforeStateRaw,
1001
+ state_raw_after: readState(),
1002
+ tracking_raw_before: beforeTrackingRaw,
1003
+ tracking_raw_after: readTrackingRaw(),
1004
+ });
1005
+
1006
+ const result = {
910
1007
  ok: true,
911
1008
  action: "init",
912
1009
  project: opts.project,
@@ -914,10 +1011,19 @@ function cmdInit(opts) {
914
1011
  total_phases: totalPhases,
915
1012
  status: "setup",
916
1013
  next_command: "/qualia-plan 1",
917
- });
1014
+ };
1015
+ if (ledger.ok) {
1016
+ result.ledger_event_id = ledger.event_id;
1017
+ result.ledger_event_hash = ledger.event_hash;
1018
+ } else {
1019
+ result.ledger_error = ledger.error;
1020
+ }
1021
+ output(result);
918
1022
  }
919
1023
 
920
1024
  function cmdFix(opts) {
1025
+ const beforeStateRaw = readState();
1026
+ const beforeTrackingRaw = readTrackingRaw();
921
1027
  const raw = readState();
922
1028
  const t = readTracking();
923
1029
  if (!raw && !t) {
@@ -980,17 +1086,41 @@ function cmdFix(opts) {
980
1086
  return output(fail("WRITE_ERROR", e.message));
981
1087
  }
982
1088
 
983
- output({
1089
+ const ledger = recordLedgerEvent({
1090
+ action: "fix",
1091
+ phase_before: parsed.phase,
1092
+ phase_after: s.phase,
1093
+ status_before: parsed.status,
1094
+ status_after: s.status,
1095
+ state_before: parsed,
1096
+ state_after: s,
1097
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1098
+ tracking_after: t,
1099
+ state_raw_before: beforeStateRaw,
1100
+ state_raw_after: readState(),
1101
+ tracking_raw_before: beforeTrackingRaw,
1102
+ tracking_raw_after: readTrackingRaw(),
1103
+ });
1104
+
1105
+ const result = {
984
1106
  ok: true,
985
1107
  action: "fix",
986
1108
  previous_errors: previousErrors,
987
1109
  fixed: true,
988
- });
1110
+ };
1111
+ if (ledger.ok) {
1112
+ result.ledger_event_id = ledger.event_id;
1113
+ result.ledger_event_hash = ledger.event_hash;
1114
+ } else {
1115
+ result.ledger_error = ledger.error;
1116
+ }
1117
+ output(result);
989
1118
  }
990
1119
 
991
1120
  function cmdValidatePlan(opts) {
992
1121
  const phase = parseInt(opts.phase) || 1;
993
1122
  const planFile = path.join(PLANNING, `phase-${phase}-plan.md`);
1123
+ const contractFile = path.join(PLANNING, `phase-${phase}-contract.json`);
994
1124
 
995
1125
  if (!fs.existsSync(planFile)) {
996
1126
  return output(fail("MISSING_FILE", `Plan file not found: ${planFile}`));
@@ -1036,6 +1166,47 @@ function cmdValidatePlan(opts) {
1036
1166
  const warnings = [];
1037
1167
  const VALID_CHECK_TYPES = ["file-exists", "grep-match", "command-exit", "behavioral"];
1038
1168
  let contractCount = 0;
1169
+ let contractStatus = "missing";
1170
+ let contractErrors = [];
1171
+
1172
+ // JSON contracts are the new machine-readable trust layer. Keep missing
1173
+ // contracts as a warning by default for in-flight legacy projects; callers can
1174
+ // opt into the hard gate with --require-contract.
1175
+ try {
1176
+ const pc = require("./plan-contract.js");
1177
+ if (fs.existsSync(contractFile)) {
1178
+ const loaded = pc.readContractFile(contractFile);
1179
+ if (!loaded.ok) {
1180
+ contractStatus = "invalid";
1181
+ contractErrors = [loaded.message || loaded.error || "contract unreadable"];
1182
+ } else {
1183
+ contractErrors = pc.validate(loaded.contract);
1184
+ const drift = pc.checkDrift(contractFile, planFile);
1185
+ if (contractErrors.length > 0) {
1186
+ contractStatus = "invalid";
1187
+ } else if (drift.ok && drift.drift) {
1188
+ contractStatus = "drifted";
1189
+ contractErrors = [`source_plan_hash drift: stored ${drift.stored}, current ${drift.current}`];
1190
+ } else {
1191
+ contractStatus = "valid";
1192
+ contractCount = Array.isArray(loaded.contract.tasks) ? loaded.contract.tasks.length : 0;
1193
+ }
1194
+ }
1195
+ }
1196
+ } catch (e) {
1197
+ contractStatus = "unavailable";
1198
+ contractErrors = [e.message];
1199
+ }
1200
+
1201
+ if (contractStatus === "missing") {
1202
+ const msg = `JSON contract missing: ${contractFile}`;
1203
+ if (opts.require_contract) errors.push(msg);
1204
+ else warnings.push(msg);
1205
+ } else if (contractStatus !== "valid") {
1206
+ const msg = `JSON contract ${contractStatus}: ${contractErrors.join("; ")}`;
1207
+ if (opts.require_contract) errors.push(msg);
1208
+ else warnings.push(msg);
1209
+ }
1039
1210
 
1040
1211
  if (/^## Verification Contract/m.test(content)) {
1041
1212
  // Extract the contract section (from header to next ## or end of file)
@@ -1049,9 +1220,10 @@ function cmdValidatePlan(opts) {
1049
1220
  if (nextH2 !== -1) contractSection = contractSection.substring(0, nextH2);
1050
1221
  // Each contract starts with ### Contract for Task N
1051
1222
  const contractBlocks = contractSection.match(/^### Contract for Task \d+/gm);
1052
- contractCount = contractBlocks ? contractBlocks.length : 0;
1223
+ const markdownContractCount = contractBlocks ? contractBlocks.length : 0;
1224
+ if (contractStatus !== "valid") contractCount = markdownContractCount;
1053
1225
 
1054
- if (contractCount === 0) {
1226
+ if (markdownContractCount === 0) {
1055
1227
  warnings.push("Verification Contract section exists but contains no contract blocks (expected '### Contract for Task N')");
1056
1228
  } else {
1057
1229
  // Split into individual contract blocks for validation
@@ -1092,9 +1264,9 @@ function cmdValidatePlan(opts) {
1092
1264
  }
1093
1265
 
1094
1266
  // Warn if contract count < task count
1095
- if (taskCount > 0 && contractCount > 0 && contractCount < taskCount) {
1267
+ if (taskCount > 0 && markdownContractCount > 0 && markdownContractCount < taskCount) {
1096
1268
  warnings.push(
1097
- `Only ${contractCount} contract(s) for ${taskCount} task(s) — not all tasks have verification contracts`
1269
+ `Only ${markdownContractCount} Markdown contract(s) for ${taskCount} task(s) — not all tasks have verification contracts`
1098
1270
  );
1099
1271
  }
1100
1272
  }
@@ -1119,6 +1291,9 @@ function cmdValidatePlan(opts) {
1119
1291
  done_when_count: doneWhenCount,
1120
1292
  ac_count: acCount,
1121
1293
  contract_count: contractCount,
1294
+ contract_file: contractFile,
1295
+ contract_status: contractStatus,
1296
+ contract_errors: contractErrors.length > 0 ? contractErrors : undefined,
1122
1297
  warnings: warnings.length > 0 ? warnings : undefined,
1123
1298
  });
1124
1299
  }
@@ -1129,6 +1304,8 @@ function cmdValidatePlan(opts) {
1129
1304
  // hiccup) does NOT double-count. To re-close a milestone deliberately, pass
1130
1305
  // --force.
1131
1306
  function cmdCloseMilestone(opts) {
1307
+ const beforeStateRaw = readState();
1308
+ const beforeTrackingRaw = readTrackingRaw();
1132
1309
  const t = readTracking();
1133
1310
  const s = parseStateMd(readState());
1134
1311
  if (!t || !s) {
@@ -1252,19 +1429,44 @@ function cmdCloseMilestone(opts) {
1252
1429
  lifetime: t.lifetime,
1253
1430
  });
1254
1431
 
1255
- output({
1432
+ const ledger = recordLedgerEvent({
1433
+ action: "close-milestone",
1434
+ phase_before: s.phase,
1435
+ phase_after: s.phase,
1436
+ status_before: s.status,
1437
+ status_after: s.status,
1438
+ state_before: s,
1439
+ state_after: s,
1440
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1441
+ tracking_after: t,
1442
+ state_raw_before: beforeStateRaw,
1443
+ state_raw_after: readState(),
1444
+ tracking_raw_before: beforeTrackingRaw,
1445
+ tracking_raw_after: readTrackingRaw(),
1446
+ });
1447
+
1448
+ const result = {
1256
1449
  ok: true,
1257
1450
  action: "close-milestone",
1258
1451
  closed_milestone: closedMilestone,
1259
1452
  next_milestone: t.milestone,
1260
1453
  lifetime: t.lifetime,
1261
- });
1454
+ };
1455
+ if (ledger.ok) {
1456
+ result.ledger_event_id = ledger.event_id;
1457
+ result.ledger_event_hash = ledger.event_hash;
1458
+ } else {
1459
+ result.ledger_error = ledger.error;
1460
+ }
1461
+ output(result);
1262
1462
  }
1263
1463
 
1264
1464
  // ─── Backfill Lifetime ───────────────────────────────────
1265
1465
  // Reconstructs lifetime counters from STATE.md roadmap + plan files.
1266
1466
  // Safe to run multiple times (idempotent — recalculates from source).
1267
1467
  function cmdBackfillLifetime(opts) {
1468
+ const beforeStateRaw = readState();
1469
+ const beforeTrackingRaw = readTrackingRaw();
1268
1470
  const t = readTracking();
1269
1471
  const s = parseStateMd(readState());
1270
1472
  if (!t || !s) {
@@ -1327,7 +1529,23 @@ function cmdBackfillLifetime(opts) {
1327
1529
  phases_scanned: s.phases.length,
1328
1530
  });
1329
1531
 
1330
- output({
1532
+ const ledger = recordLedgerEvent({
1533
+ action: "backfill-lifetime",
1534
+ phase_before: s.phase,
1535
+ phase_after: s.phase,
1536
+ status_before: s.status,
1537
+ status_after: s.status,
1538
+ state_before: s,
1539
+ state_after: s,
1540
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1541
+ tracking_after: t,
1542
+ state_raw_before: beforeStateRaw,
1543
+ state_raw_after: readState(),
1544
+ tracking_raw_before: beforeTrackingRaw,
1545
+ tracking_raw_after: readTrackingRaw(),
1546
+ });
1547
+
1548
+ const result = {
1331
1549
  ok: true,
1332
1550
  action: "backfill-lifetime",
1333
1551
  previous,
@@ -1335,7 +1553,14 @@ function cmdBackfillLifetime(opts) {
1335
1553
  phases_scanned: s.phases.length,
1336
1554
  phases_completed: phasesCompleted,
1337
1555
  tasks_completed: tasksCompleted,
1338
- });
1556
+ };
1557
+ if (ledger.ok) {
1558
+ result.ledger_event_id = ledger.event_id;
1559
+ result.ledger_event_hash = ledger.event_hash;
1560
+ } else {
1561
+ result.ledger_error = ledger.error;
1562
+ }
1563
+ output(result);
1339
1564
  }
1340
1565
 
1341
1566
  // ─── Backfill Milestones from JOURNEY.md ─────────────────
@@ -1353,6 +1578,8 @@ function cmdBackfillLifetime(opts) {
1353
1578
  // Phase counting handles ranges (`1–13` → 13), comma lists (`14, 15, 16.1–16.6` → 8),
1354
1579
  // and "rolling" / "—" / "-" → 0.
1355
1580
  function cmdBackfillMilestones(opts) {
1581
+ const beforeStateRaw = readState();
1582
+ const beforeTrackingRaw = readTrackingRaw();
1356
1583
  const t = readTracking();
1357
1584
  if (!t) return output(fail("NO_PROJECT", "No .planning/ found."));
1358
1585
  ensureLifetime(t);
@@ -1507,7 +1734,24 @@ function cmdBackfillMilestones(opts) {
1507
1734
  lifetime: t.lifetime,
1508
1735
  });
1509
1736
 
1510
- output({
1737
+ const afterState = parseStateMd(readState());
1738
+ const ledger = recordLedgerEvent({
1739
+ action: "backfill-milestones",
1740
+ phase_before: afterState ? afterState.phase : undefined,
1741
+ phase_after: afterState ? afterState.phase : undefined,
1742
+ status_before: afterState ? afterState.status : undefined,
1743
+ status_after: afterState ? afterState.status : undefined,
1744
+ state_before: parseStateMd(beforeStateRaw),
1745
+ state_after: afterState,
1746
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1747
+ tracking_after: t,
1748
+ state_raw_before: beforeStateRaw,
1749
+ state_raw_after: readState(),
1750
+ tracking_raw_before: beforeTrackingRaw,
1751
+ tracking_raw_after: readTrackingRaw(),
1752
+ });
1753
+
1754
+ const result = {
1511
1755
  ok: true,
1512
1756
  action: "backfill-milestones",
1513
1757
  added,
@@ -1515,7 +1759,14 @@ function cmdBackfillMilestones(opts) {
1515
1759
  closed: closedSummaries,
1516
1760
  open_milestone: openRow ? { num: openRow.num, name: openRow.name } : null,
1517
1761
  lifetime: t.lifetime,
1518
- });
1762
+ };
1763
+ if (ledger.ok) {
1764
+ result.ledger_event_id = ledger.event_id;
1765
+ result.ledger_event_hash = ledger.event_hash;
1766
+ } else {
1767
+ result.ledger_error = ledger.error;
1768
+ }
1769
+ output(result);
1519
1770
  }
1520
1771
 
1521
1772
  // ─── Next Report ID ──────────────────────────────────────
@@ -1525,6 +1776,8 @@ function cmdBackfillMilestones(opts) {
1525
1776
  // to the ERP. If --peek is passed, the next id is returned WITHOUT
1526
1777
  // incrementing — useful for --dry-run previews.
1527
1778
  function cmdNextReportId(opts) {
1779
+ const beforeStateRaw = readState();
1780
+ const beforeTrackingRaw = readTrackingRaw();
1528
1781
  const t = readTracking();
1529
1782
  if (!t) return output(fail("NO_PROJECT", "No .planning/ found."));
1530
1783
  ensureLifetime(t);
@@ -1536,7 +1789,33 @@ function cmdNextReportId(opts) {
1536
1789
  t.last_updated = new Date().toISOString();
1537
1790
  writeTracking(t);
1538
1791
  }
1539
- output({ ok: true, action: "next-report-id", report_id: id, report_seq: next, peeked: peek });
1792
+ const result = { ok: true, action: "next-report-id", report_id: id, report_seq: next, peeked: peek };
1793
+ if (!peek) {
1794
+ const afterState = parseStateMd(readState());
1795
+ const beforeState = parseStateMd(beforeStateRaw);
1796
+ const ledger = recordLedgerEvent({
1797
+ action: "next-report-id",
1798
+ phase_before: beforeState ? beforeState.phase : undefined,
1799
+ phase_after: afterState ? afterState.phase : undefined,
1800
+ status_before: beforeState ? beforeState.status : undefined,
1801
+ status_after: afterState ? afterState.status : undefined,
1802
+ state_before: beforeState,
1803
+ state_after: afterState,
1804
+ tracking_before: parseTrackingRaw(beforeTrackingRaw),
1805
+ tracking_after: t,
1806
+ state_raw_before: beforeStateRaw,
1807
+ state_raw_after: readState(),
1808
+ tracking_raw_before: beforeTrackingRaw,
1809
+ tracking_raw_after: readTrackingRaw(),
1810
+ });
1811
+ if (ledger.ok) {
1812
+ result.ledger_event_id = ledger.event_id;
1813
+ result.ledger_event_hash = ledger.event_hash;
1814
+ } else {
1815
+ result.ledger_error = ledger.error;
1816
+ }
1817
+ }
1818
+ output(result);
1540
1819
  }
1541
1820
 
1542
1821
  // ─── Output ──────────────────────────────────────────────