akm-cli 0.9.17 → 0.9.19-alpha.1

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 (57) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/STABILITY.md +2 -1
  3. package/dist/assets/hints/cli-hints-full.md +4 -2
  4. package/dist/assets/hints/cli-hints-short.md +5 -3
  5. package/dist/assets/prompts/reflect-feedback-framing.md +1 -1
  6. package/dist/commands/feedback-cli.js +1 -1
  7. package/dist/commands/health/checks.js +6 -6
  8. package/dist/commands/health.js +3 -3
  9. package/dist/commands/improve/consolidate/coverage.js +132 -0
  10. package/dist/commands/improve/consolidate/pair-pass.js +31 -21
  11. package/dist/commands/improve/consolidate.js +46 -13
  12. package/dist/commands/improve/distill.js +8 -4
  13. package/dist/commands/improve/eligibility.js +39 -9
  14. package/dist/commands/improve/improve-cli.js +9 -6
  15. package/dist/commands/improve/improve.js +13 -4
  16. package/dist/commands/improve/ledger.js +2 -2
  17. package/dist/commands/improve/loop-stages.js +2 -0
  18. package/dist/commands/improve/preparation.js +3 -1
  19. package/dist/commands/improve/reflect.js +2 -2
  20. package/dist/commands/improve/stage.js +43 -12
  21. package/dist/commands/proposal/diff-format.js +21 -0
  22. package/dist/commands/proposal/proposal-cli.js +48 -10
  23. package/dist/commands/proposal/proposal-types.js +11 -0
  24. package/dist/commands/proposal/proposal.js +60 -5
  25. package/dist/commands/proposal/repository.js +250 -18
  26. package/dist/commands/read/knowledge.js +13 -11
  27. package/dist/commands/read/remember-cli.js +7 -3
  28. package/dist/commands/sources/source-clone.js +1 -1
  29. package/dist/commands/tasks/tasks-cli.js +1 -1
  30. package/dist/commands/tasks/tasks.js +10 -3
  31. package/dist/core/mutation-target.js +8 -3
  32. package/dist/core/write-source.js +3 -2
  33. package/dist/indexer/usage/usage-events.js +2 -1
  34. package/dist/output/shapes/helpers.js +7 -0
  35. package/dist/output/shapes/passthrough.js +1 -0
  36. package/dist/output/shapes/proposal/reopen.js +14 -0
  37. package/dist/output/shapes.js +2 -0
  38. package/dist/output/text/helpers.js +1 -1
  39. package/dist/output/text/proposal/proposal.js +3 -1
  40. package/dist/output/text/proposal-format.js +87 -32
  41. package/dist/scripts/akm-migrate-node.js +102 -25
  42. package/dist/scripts/akm-migrate.js +102 -25
  43. package/dist/storage/repositories/improve-ledger-repository.js +65 -6
  44. package/dist/storage/repositories/index-vec-repository.js +13 -8
  45. package/dist/storage/repositories/proposals-repository.js +23 -0
  46. package/dist/storage/sqlite-read-snapshot.js +46 -2
  47. package/dist/storage/state-db-integrity.js +12 -9
  48. package/dist/tasks/run/load-task.js +5 -1
  49. package/docs/migration/README.md +1 -0
  50. package/docs/migration/release-notes/0.9.19.md +134 -0
  51. package/docs/migration/release-notes/README.md +5 -0
  52. package/docs/migration/v0.7-to-v0.8.md +2 -2
  53. package/docs/migration/v0.8-to-v0.9.md +5 -1
  54. package/docs/reference/cli.md +189 -28
  55. package/docs/reference/configuration.md +9 -8
  56. package/docs/reference/data-and-telemetry.md +24 -16
  57. package/package.json +1 -1
@@ -28540,6 +28540,7 @@ function shapeProposalEntry(entry, detail) {
28540
28540
  "confidence",
28541
28541
  "gateDecision",
28542
28542
  "review",
28543
+ "reviewHistory",
28543
28544
  "retirement"
28544
28545
  ]);
28545
28546
  }
@@ -28555,6 +28556,7 @@ function shapeProposalEntry(entry, detail) {
28555
28556
  "gateDecision",
28556
28557
  "payload",
28557
28558
  "review",
28559
+ "reviewHistory",
28558
28560
  "retirement",
28559
28561
  "retiredArchive",
28560
28562
  "promotionSource",
@@ -28619,7 +28621,10 @@ function shapeProposalDiffOutput(result, detail) {
28619
28621
  ref: result.ref,
28620
28622
  isNew: result.isNew,
28621
28623
  unified: result.unified,
28622
- ...result.targetPath !== undefined ? { targetPath: result.targetPath } : {}
28624
+ ...result.targetPath !== undefined ? { targetPath: result.targetPath } : {},
28625
+ ...result.op !== undefined ? { op: result.op } : {},
28626
+ ...result.retirement !== undefined ? { retirement: result.retirement } : {},
28627
+ ...result.note !== undefined ? { note: result.note } : {}
28623
28628
  };
28624
28629
  if (detail === "full") {
28625
28630
  return { schemaVersion: result.schemaVersion, ...base };
@@ -29041,6 +29046,7 @@ var PASSTHROUGH_COMMANDS = [
29041
29046
  "proposal-accept-batch",
29042
29047
  "proposal-drain",
29043
29048
  "proposal-reject-batch",
29049
+ "proposal-reopen-batch",
29044
29050
  "proposal-revert",
29045
29051
  "registry-add",
29046
29052
  "registry-list",
@@ -29112,6 +29118,14 @@ var proposalRejectShapes = [
29112
29118
  }
29113
29119
  ];
29114
29120
 
29121
+ // src/output/shapes/proposal/reopen.ts
29122
+ var proposalReopenShapes = [
29123
+ {
29124
+ command: "proposal-reopen",
29125
+ handler: (result, detail) => shapeProposalRejectOutput(result, detail)
29126
+ }
29127
+ ];
29128
+
29115
29129
  // src/output/shapes/proposal/show.ts
29116
29130
  var proposalShowShapes = [
29117
29131
  {
@@ -29202,6 +29216,7 @@ var BUILT_IN_OUTPUT_SHAPES = [
29202
29216
  ...proposalShowShapes,
29203
29217
  ...proposalAcceptShapes,
29204
29218
  ...proposalRejectShapes,
29219
+ ...proposalReopenShapes,
29205
29220
  ...proposalDiffShapes,
29206
29221
  ...proposalProducerShapes,
29207
29222
  ...envListShapes,
@@ -30423,6 +30438,31 @@ Generate one with \`akm improve <ref>\` or \`akm proposal new <type> <name> --ta
30423
30438
  return lines.join(`
30424
30439
  `).trimEnd();
30425
30440
  }
30441
+ function retireVerdictLines(retirement) {
30442
+ const lines = [
30443
+ `retire.label: ${String(retirement.judgeLabel)} (cosine=${String(retirement.cosine)})`,
30444
+ `retire.reason: ${String(retirement.judgeReason)}`
30445
+ ];
30446
+ const continuityRisk = retirement.continuityRisk;
30447
+ if (continuityRisk) {
30448
+ const failingQueries = typeof continuityRisk.failingQueries === "number" ? continuityRisk.failingQueries : 0;
30449
+ const unverifiedQueries = typeof continuityRisk.unverifiedQueries === "number" ? continuityRisk.unverifiedQueries : 0;
30450
+ const summary = [];
30451
+ if (failingQueries > 0) {
30452
+ summary.push(`${failingQueries} of the retired asset's own quer${failingQueries === 1 ? "y" : "ies"} would not have found the successor top 10`);
30453
+ }
30454
+ if (unverifiedQueries > 0) {
30455
+ summary.push(`${unverifiedQueries} quer${unverifiedQueries === 1 ? "y" : "ies"} unverified (search failed or used the keyword-only fallback)`);
30456
+ }
30457
+ lines.push(`retire.continuityRisk: ${summary.join("; ")} — excluded from bulk accept`);
30458
+ const ranks = Array.isArray(continuityRisk.ranks) ? continuityRisk.ranks : [];
30459
+ for (const rank of ranks) {
30460
+ const successorRank = rank.successorRank === null || rank.successorRank === undefined ? "absent from top 10" : `#${String(rank.successorRank)}`;
30461
+ lines.push(` - "${String(rank.query)}": retired #${String(rank.retiredRank)}, successor ${successorRank}`);
30462
+ }
30463
+ }
30464
+ return lines;
30465
+ }
30426
30466
  function formatProposalShowPlain(r) {
30427
30467
  const p = r.proposal;
30428
30468
  const lines = [];
@@ -30458,29 +30498,17 @@ function formatProposalShowPlain(r) {
30458
30498
  if (review.decidedAt)
30459
30499
  lines.push(`review.decidedAt: ${String(review.decidedAt)}`);
30460
30500
  }
30501
+ const history = Array.isArray(p.reviewHistory) ? p.reviewHistory : [];
30502
+ for (const entry of history) {
30503
+ const undone = entry.review;
30504
+ const was = undone ? `${String(undone.outcome ?? "?")}${undone.reason ? `: ${String(undone.reason)}` : ""} (${String(undone.decidedAt ?? "?")})` : "an unrecorded review";
30505
+ const why = entry.reopenReason ? ` (${String(entry.reopenReason)})` : "";
30506
+ lines.push(`reopened: ${String(entry.reopenedAt)}${why}, undoing ${was}`);
30507
+ }
30461
30508
  const retirement = p.retirement;
30462
30509
  if (retirement) {
30463
30510
  lines.push(`retire: ${String(retirement.retiredRef)} -> ${String(retirement.successorRef)}`);
30464
- lines.push(`retire.label: ${String(retirement.judgeLabel)} (cosine=${String(retirement.cosine)})`);
30465
- lines.push(`retire.reason: ${String(retirement.judgeReason)}`);
30466
- const continuityRisk = retirement.continuityRisk;
30467
- if (continuityRisk) {
30468
- const failingQueries = typeof continuityRisk.failingQueries === "number" ? continuityRisk.failingQueries : 0;
30469
- const unverifiedQueries = typeof continuityRisk.unverifiedQueries === "number" ? continuityRisk.unverifiedQueries : 0;
30470
- const summary = [];
30471
- if (failingQueries > 0) {
30472
- summary.push(`${failingQueries} of the retired asset's own quer${failingQueries === 1 ? "y" : "ies"} would not have found the successor top 10`);
30473
- }
30474
- if (unverifiedQueries > 0) {
30475
- summary.push(`${unverifiedQueries} quer${unverifiedQueries === 1 ? "y" : "ies"} unverified (search failed or used the keyword-only fallback)`);
30476
- }
30477
- lines.push(`retire.continuityRisk: ${summary.join("; ")} — excluded from bulk accept`);
30478
- const ranks = Array.isArray(continuityRisk.ranks) ? continuityRisk.ranks : [];
30479
- for (const rank of ranks) {
30480
- const successorRank = rank.successorRank === null || rank.successorRank === undefined ? "absent from top 10" : `#${String(rank.successorRank)}`;
30481
- lines.push(` - "${String(rank.query)}": retired #${String(rank.retiredRank)}, successor ${successorRank}`);
30482
- }
30483
- }
30511
+ lines.push(...retireVerdictLines(retirement));
30484
30512
  }
30485
30513
  const validation = r.validation;
30486
30514
  if (validation) {
@@ -30504,7 +30532,7 @@ function formatProposalShowPlain(r) {
30504
30532
  }
30505
30533
  }
30506
30534
  const payload = p.payload;
30507
- if (payload && typeof payload.content === "string") {
30535
+ if (!retirement && payload && typeof payload.content === "string") {
30508
30536
  lines.push("");
30509
30537
  lines.push("payload:");
30510
30538
  lines.push(payload.content);
@@ -30519,6 +30547,19 @@ function formatProposalRejectPlain(r) {
30519
30547
  const reason = r.reason ? ` (${String(r.reason)})` : "";
30520
30548
  return `Rejected proposal ${String(r.id)} (${String(r.ref)})${reason}`;
30521
30549
  }
30550
+ function formatProposalReopenPlain(r) {
30551
+ const reason = r.reason ? ` (${String(r.reason)})` : "";
30552
+ return `Reopened proposal ${String(r.id)} (${String(r.ref)}) [pending]${reason}`;
30553
+ }
30554
+ function formatProposalReopenBatchPlain(r) {
30555
+ const results = Array.isArray(r.results) ? r.results : [];
30556
+ const reason = results[0]?.reason ? ` (${String(results[0].reason)})` : "";
30557
+ return [
30558
+ `Reopened ${results.length} proposal(s) [pending]${reason}`,
30559
+ ...results.map((result) => ` ${String(result.id)} ${String(result.ref)}`)
30560
+ ].join(`
30561
+ `);
30562
+ }
30522
30563
  function formatProposalDrainPlain(r) {
30523
30564
  const applyMode = String(r.applyMode ?? "queue");
30524
30565
  const promoted = Array.isArray(r.promoted) ? r.promoted : [];
@@ -30548,8 +30589,21 @@ function formatProposalDrainPlain(r) {
30548
30589
  `).trimEnd();
30549
30590
  }
30550
30591
  function formatProposalDiffPlain(r) {
30551
- const header = r.isNew ? `# proposal ${String(r.id)} (new asset: ${String(r.ref)})` : `# proposal ${String(r.id)} (update: ${String(r.ref)})`;
30552
30592
  const unified = typeof r.unified === "string" ? r.unified : "";
30593
+ if (r.op === "delete") {
30594
+ const retirement = r.retirement;
30595
+ const subject = retirement ? `${String(retirement.retiredRef)} -> ${String(retirement.successorRef)}` : String(r.ref);
30596
+ const lines = [`# proposal ${String(r.id)} (retire: ${subject})`];
30597
+ if (retirement)
30598
+ lines.push(...retireVerdictLines(retirement));
30599
+ if (typeof r.note === "string")
30600
+ lines.push(`note: ${r.note}`);
30601
+ if (unified)
30602
+ lines.push(unified);
30603
+ return lines.join(`
30604
+ `);
30605
+ }
30606
+ const header = r.isNew ? `# proposal ${String(r.id)} (new asset: ${String(r.ref)})` : `# proposal ${String(r.id)} (update: ${String(r.ref)})`;
30553
30607
  if (!unified)
30554
30608
  return `${header}
30555
30609
  (no changes)`;
@@ -52064,6 +52118,7 @@ import os6 from "node:os";
52064
52118
  import path44 from "node:path";
52065
52119
 
52066
52120
  // src/storage/sqlite-read-snapshot.ts
52121
+ import { spawnSync as spawnSync5 } from "node:child_process";
52067
52122
  import fs36 from "node:fs";
52068
52123
  import os5 from "node:os";
52069
52124
  import path43 from "node:path";
@@ -52108,6 +52163,26 @@ function fingerprintsEqual(left, right) {
52108
52163
  const sameFile = (a, b) => a?.size === b?.size && a?.mtimeNs === b?.mtimeNs && a?.ctimeNs === b?.ctimeNs;
52109
52164
  return sameFile(left.main, right.main) && sameFile(left.wal, right.wal);
52110
52165
  }
52166
+ function copyFileOutsideThisProcess(source, destination) {
52167
+ if (process.platform === "win32") {
52168
+ fs36.copyFileSync(source, destination);
52169
+ return;
52170
+ }
52171
+ const result = spawnSync5("cp", ["--", source, destination], {
52172
+ encoding: "utf8",
52173
+ stdio: ["ignore", "ignore", "pipe"]
52174
+ });
52175
+ if (result.error) {
52176
+ throw new SqliteReadSnapshotUnavailableError(`cannot run cp to copy ${source}: ${result.error.message}`);
52177
+ }
52178
+ if (result.status === 0)
52179
+ return;
52180
+ if (fileFingerprint(source) === undefined) {
52181
+ throw Object.assign(new Error(`${source} disappeared while it was being copied`), { code: "ENOENT" });
52182
+ }
52183
+ const reason = result.stderr.trim() || (result.signal ? `killed by ${result.signal}` : `exit status ${result.status}`);
52184
+ throw new Error(`cp could not copy ${source}: ${reason}`);
52185
+ }
52111
52186
  function openSqliteReadSnapshot(dbPath) {
52112
52187
  if (!pathExists(dbPath))
52113
52188
  return;
@@ -52123,9 +52198,9 @@ function openSqliteReadSnapshot(dbPath) {
52123
52198
  if (pathExists(`${dbPath}-journal`))
52124
52199
  continue;
52125
52200
  const before = databaseFingerprint(dbPath);
52126
- fs36.copyFileSync(dbPath, snapshotPath);
52201
+ copyFileOutsideThisProcess(dbPath, snapshotPath);
52127
52202
  if (before.wal)
52128
- fs36.copyFileSync(`${dbPath}-wal`, `${snapshotPath}-wal`);
52203
+ copyFileOutsideThisProcess(`${dbPath}-wal`, `${snapshotPath}-wal`);
52129
52204
  else
52130
52205
  fs36.rmSync(`${snapshotPath}-wal`, { force: true });
52131
52206
  const after = databaseFingerprint(dbPath);
@@ -52894,6 +52969,8 @@ var proposalFormatters = [
52894
52969
  { command: "proposal-show", handler: (r) => formatProposalShowPlain(r) },
52895
52970
  { command: "proposal-accept", handler: (r) => formatProposalAcceptPlain(r) },
52896
52971
  { command: "proposal-reject", handler: (r) => formatProposalRejectPlain(r) },
52972
+ { command: "proposal-reopen", handler: (r) => formatProposalReopenPlain(r) },
52973
+ { command: "proposal-reopen-batch", handler: (r) => formatProposalReopenBatchPlain(r) },
52897
52974
  { command: "proposal-diff", handler: (r) => formatProposalDiffPlain(r) },
52898
52975
  { command: "proposal-drain", handler: (r) => formatProposalDrainPlain(r) }
52899
52976
  ];
@@ -27868,6 +27868,7 @@ function shapeProposalEntry(entry, detail) {
27868
27868
  "confidence",
27869
27869
  "gateDecision",
27870
27870
  "review",
27871
+ "reviewHistory",
27871
27872
  "retirement"
27872
27873
  ]);
27873
27874
  }
@@ -27883,6 +27884,7 @@ function shapeProposalEntry(entry, detail) {
27883
27884
  "gateDecision",
27884
27885
  "payload",
27885
27886
  "review",
27887
+ "reviewHistory",
27886
27888
  "retirement",
27887
27889
  "retiredArchive",
27888
27890
  "promotionSource",
@@ -27947,7 +27949,10 @@ function shapeProposalDiffOutput(result, detail) {
27947
27949
  ref: result.ref,
27948
27950
  isNew: result.isNew,
27949
27951
  unified: result.unified,
27950
- ...result.targetPath !== undefined ? { targetPath: result.targetPath } : {}
27952
+ ...result.targetPath !== undefined ? { targetPath: result.targetPath } : {},
27953
+ ...result.op !== undefined ? { op: result.op } : {},
27954
+ ...result.retirement !== undefined ? { retirement: result.retirement } : {},
27955
+ ...result.note !== undefined ? { note: result.note } : {}
27951
27956
  };
27952
27957
  if (detail === "full") {
27953
27958
  return { schemaVersion: result.schemaVersion, ...base };
@@ -28369,6 +28374,7 @@ var PASSTHROUGH_COMMANDS = [
28369
28374
  "proposal-accept-batch",
28370
28375
  "proposal-drain",
28371
28376
  "proposal-reject-batch",
28377
+ "proposal-reopen-batch",
28372
28378
  "proposal-revert",
28373
28379
  "registry-add",
28374
28380
  "registry-list",
@@ -28440,6 +28446,14 @@ var proposalRejectShapes = [
28440
28446
  }
28441
28447
  ];
28442
28448
 
28449
+ // src/output/shapes/proposal/reopen.ts
28450
+ var proposalReopenShapes = [
28451
+ {
28452
+ command: "proposal-reopen",
28453
+ handler: (result, detail) => shapeProposalRejectOutput(result, detail)
28454
+ }
28455
+ ];
28456
+
28443
28457
  // src/output/shapes/proposal/show.ts
28444
28458
  var proposalShowShapes = [
28445
28459
  {
@@ -28530,6 +28544,7 @@ var BUILT_IN_OUTPUT_SHAPES = [
28530
28544
  ...proposalShowShapes,
28531
28545
  ...proposalAcceptShapes,
28532
28546
  ...proposalRejectShapes,
28547
+ ...proposalReopenShapes,
28533
28548
  ...proposalDiffShapes,
28534
28549
  ...proposalProducerShapes,
28535
28550
  ...envListShapes,
@@ -29751,6 +29766,31 @@ Generate one with \`akm improve <ref>\` or \`akm proposal new <type> <name> --ta
29751
29766
  return lines.join(`
29752
29767
  `).trimEnd();
29753
29768
  }
29769
+ function retireVerdictLines(retirement) {
29770
+ const lines = [
29771
+ `retire.label: ${String(retirement.judgeLabel)} (cosine=${String(retirement.cosine)})`,
29772
+ `retire.reason: ${String(retirement.judgeReason)}`
29773
+ ];
29774
+ const continuityRisk = retirement.continuityRisk;
29775
+ if (continuityRisk) {
29776
+ const failingQueries = typeof continuityRisk.failingQueries === "number" ? continuityRisk.failingQueries : 0;
29777
+ const unverifiedQueries = typeof continuityRisk.unverifiedQueries === "number" ? continuityRisk.unverifiedQueries : 0;
29778
+ const summary = [];
29779
+ if (failingQueries > 0) {
29780
+ summary.push(`${failingQueries} of the retired asset's own quer${failingQueries === 1 ? "y" : "ies"} would not have found the successor top 10`);
29781
+ }
29782
+ if (unverifiedQueries > 0) {
29783
+ summary.push(`${unverifiedQueries} quer${unverifiedQueries === 1 ? "y" : "ies"} unverified (search failed or used the keyword-only fallback)`);
29784
+ }
29785
+ lines.push(`retire.continuityRisk: ${summary.join("; ")} \u2014 excluded from bulk accept`);
29786
+ const ranks = Array.isArray(continuityRisk.ranks) ? continuityRisk.ranks : [];
29787
+ for (const rank of ranks) {
29788
+ const successorRank = rank.successorRank === null || rank.successorRank === undefined ? "absent from top 10" : `#${String(rank.successorRank)}`;
29789
+ lines.push(` - "${String(rank.query)}": retired #${String(rank.retiredRank)}, successor ${successorRank}`);
29790
+ }
29791
+ }
29792
+ return lines;
29793
+ }
29754
29794
  function formatProposalShowPlain(r) {
29755
29795
  const p = r.proposal;
29756
29796
  const lines = [];
@@ -29786,29 +29826,17 @@ function formatProposalShowPlain(r) {
29786
29826
  if (review.decidedAt)
29787
29827
  lines.push(`review.decidedAt: ${String(review.decidedAt)}`);
29788
29828
  }
29829
+ const history = Array.isArray(p.reviewHistory) ? p.reviewHistory : [];
29830
+ for (const entry of history) {
29831
+ const undone = entry.review;
29832
+ const was = undone ? `${String(undone.outcome ?? "?")}${undone.reason ? `: ${String(undone.reason)}` : ""} (${String(undone.decidedAt ?? "?")})` : "an unrecorded review";
29833
+ const why = entry.reopenReason ? ` (${String(entry.reopenReason)})` : "";
29834
+ lines.push(`reopened: ${String(entry.reopenedAt)}${why}, undoing ${was}`);
29835
+ }
29789
29836
  const retirement = p.retirement;
29790
29837
  if (retirement) {
29791
29838
  lines.push(`retire: ${String(retirement.retiredRef)} -> ${String(retirement.successorRef)}`);
29792
- lines.push(`retire.label: ${String(retirement.judgeLabel)} (cosine=${String(retirement.cosine)})`);
29793
- lines.push(`retire.reason: ${String(retirement.judgeReason)}`);
29794
- const continuityRisk = retirement.continuityRisk;
29795
- if (continuityRisk) {
29796
- const failingQueries = typeof continuityRisk.failingQueries === "number" ? continuityRisk.failingQueries : 0;
29797
- const unverifiedQueries = typeof continuityRisk.unverifiedQueries === "number" ? continuityRisk.unverifiedQueries : 0;
29798
- const summary = [];
29799
- if (failingQueries > 0) {
29800
- summary.push(`${failingQueries} of the retired asset's own quer${failingQueries === 1 ? "y" : "ies"} would not have found the successor top 10`);
29801
- }
29802
- if (unverifiedQueries > 0) {
29803
- summary.push(`${unverifiedQueries} quer${unverifiedQueries === 1 ? "y" : "ies"} unverified (search failed or used the keyword-only fallback)`);
29804
- }
29805
- lines.push(`retire.continuityRisk: ${summary.join("; ")} \u2014 excluded from bulk accept`);
29806
- const ranks = Array.isArray(continuityRisk.ranks) ? continuityRisk.ranks : [];
29807
- for (const rank of ranks) {
29808
- const successorRank = rank.successorRank === null || rank.successorRank === undefined ? "absent from top 10" : `#${String(rank.successorRank)}`;
29809
- lines.push(` - "${String(rank.query)}": retired #${String(rank.retiredRank)}, successor ${successorRank}`);
29810
- }
29811
- }
29839
+ lines.push(...retireVerdictLines(retirement));
29812
29840
  }
29813
29841
  const validation = r.validation;
29814
29842
  if (validation) {
@@ -29832,7 +29860,7 @@ function formatProposalShowPlain(r) {
29832
29860
  }
29833
29861
  }
29834
29862
  const payload = p.payload;
29835
- if (payload && typeof payload.content === "string") {
29863
+ if (!retirement && payload && typeof payload.content === "string") {
29836
29864
  lines.push("");
29837
29865
  lines.push("payload:");
29838
29866
  lines.push(payload.content);
@@ -29847,6 +29875,19 @@ function formatProposalRejectPlain(r) {
29847
29875
  const reason = r.reason ? ` (${String(r.reason)})` : "";
29848
29876
  return `Rejected proposal ${String(r.id)} (${String(r.ref)})${reason}`;
29849
29877
  }
29878
+ function formatProposalReopenPlain(r) {
29879
+ const reason = r.reason ? ` (${String(r.reason)})` : "";
29880
+ return `Reopened proposal ${String(r.id)} (${String(r.ref)}) [pending]${reason}`;
29881
+ }
29882
+ function formatProposalReopenBatchPlain(r) {
29883
+ const results = Array.isArray(r.results) ? r.results : [];
29884
+ const reason = results[0]?.reason ? ` (${String(results[0].reason)})` : "";
29885
+ return [
29886
+ `Reopened ${results.length} proposal(s) [pending]${reason}`,
29887
+ ...results.map((result) => ` ${String(result.id)} ${String(result.ref)}`)
29888
+ ].join(`
29889
+ `);
29890
+ }
29850
29891
  function formatProposalDrainPlain(r) {
29851
29892
  const applyMode = String(r.applyMode ?? "queue");
29852
29893
  const promoted = Array.isArray(r.promoted) ? r.promoted : [];
@@ -29876,8 +29917,21 @@ function formatProposalDrainPlain(r) {
29876
29917
  `).trimEnd();
29877
29918
  }
29878
29919
  function formatProposalDiffPlain(r) {
29879
- const header = r.isNew ? `# proposal ${String(r.id)} (new asset: ${String(r.ref)})` : `# proposal ${String(r.id)} (update: ${String(r.ref)})`;
29880
29920
  const unified = typeof r.unified === "string" ? r.unified : "";
29921
+ if (r.op === "delete") {
29922
+ const retirement = r.retirement;
29923
+ const subject = retirement ? `${String(retirement.retiredRef)} -> ${String(retirement.successorRef)}` : String(r.ref);
29924
+ const lines = [`# proposal ${String(r.id)} (retire: ${subject})`];
29925
+ if (retirement)
29926
+ lines.push(...retireVerdictLines(retirement));
29927
+ if (typeof r.note === "string")
29928
+ lines.push(`note: ${r.note}`);
29929
+ if (unified)
29930
+ lines.push(unified);
29931
+ return lines.join(`
29932
+ `);
29933
+ }
29934
+ const header = r.isNew ? `# proposal ${String(r.id)} (new asset: ${String(r.ref)})` : `# proposal ${String(r.id)} (update: ${String(r.ref)})`;
29881
29935
  if (!unified)
29882
29936
  return `${header}
29883
29937
  (no changes)`;
@@ -52059,6 +52113,7 @@ import os6 from "os";
52059
52113
  import path44 from "path";
52060
52114
 
52061
52115
  // src/storage/sqlite-read-snapshot.ts
52116
+ import { spawnSync as spawnSync5 } from "child_process";
52062
52117
  import fs36 from "fs";
52063
52118
  import os5 from "os";
52064
52119
  import path43 from "path";
@@ -52103,6 +52158,26 @@ function fingerprintsEqual(left, right) {
52103
52158
  const sameFile = (a, b) => a?.size === b?.size && a?.mtimeNs === b?.mtimeNs && a?.ctimeNs === b?.ctimeNs;
52104
52159
  return sameFile(left.main, right.main) && sameFile(left.wal, right.wal);
52105
52160
  }
52161
+ function copyFileOutsideThisProcess(source, destination) {
52162
+ if (process.platform === "win32") {
52163
+ fs36.copyFileSync(source, destination);
52164
+ return;
52165
+ }
52166
+ const result = spawnSync5("cp", ["--", source, destination], {
52167
+ encoding: "utf8",
52168
+ stdio: ["ignore", "ignore", "pipe"]
52169
+ });
52170
+ if (result.error) {
52171
+ throw new SqliteReadSnapshotUnavailableError(`cannot run cp to copy ${source}: ${result.error.message}`);
52172
+ }
52173
+ if (result.status === 0)
52174
+ return;
52175
+ if (fileFingerprint(source) === undefined) {
52176
+ throw Object.assign(new Error(`${source} disappeared while it was being copied`), { code: "ENOENT" });
52177
+ }
52178
+ const reason = result.stderr.trim() || (result.signal ? `killed by ${result.signal}` : `exit status ${result.status}`);
52179
+ throw new Error(`cp could not copy ${source}: ${reason}`);
52180
+ }
52106
52181
  function openSqliteReadSnapshot(dbPath) {
52107
52182
  if (!pathExists(dbPath))
52108
52183
  return;
@@ -52118,9 +52193,9 @@ function openSqliteReadSnapshot(dbPath) {
52118
52193
  if (pathExists(`${dbPath}-journal`))
52119
52194
  continue;
52120
52195
  const before = databaseFingerprint(dbPath);
52121
- fs36.copyFileSync(dbPath, snapshotPath);
52196
+ copyFileOutsideThisProcess(dbPath, snapshotPath);
52122
52197
  if (before.wal)
52123
- fs36.copyFileSync(`${dbPath}-wal`, `${snapshotPath}-wal`);
52198
+ copyFileOutsideThisProcess(`${dbPath}-wal`, `${snapshotPath}-wal`);
52124
52199
  else
52125
52200
  fs36.rmSync(`${snapshotPath}-wal`, { force: true });
52126
52201
  const after = databaseFingerprint(dbPath);
@@ -52889,6 +52964,8 @@ var proposalFormatters = [
52889
52964
  { command: "proposal-show", handler: (r) => formatProposalShowPlain(r) },
52890
52965
  { command: "proposal-accept", handler: (r) => formatProposalAcceptPlain(r) },
52891
52966
  { command: "proposal-reject", handler: (r) => formatProposalRejectPlain(r) },
52967
+ { command: "proposal-reopen", handler: (r) => formatProposalReopenPlain(r) },
52968
+ { command: "proposal-reopen-batch", handler: (r) => formatProposalReopenBatchPlain(r) },
52892
52969
  { command: "proposal-diff", handler: (r) => formatProposalDiffPlain(r) },
52893
52970
  { command: "proposal-drain", handler: (r) => formatProposalDrainPlain(r) }
52894
52971
  ];
@@ -38,6 +38,33 @@ export const LEDGER_REVISIT_CADENCE_DAYS = 7;
38
38
  * eligible again.
39
39
  */
40
40
  export const PAIR_PASS_LEDGER_SOURCE = "consolidate-pair";
41
+ /**
42
+ * The consolidate promote pass's ledger source: one row per source memory,
43
+ * keyed by the memory (`memories/<name>`), not by the knowledge ref the
44
+ * promotion would create.
45
+ */
46
+ export const CONSOLIDATE_LEDGER_SOURCE = "consolidate";
47
+ /**
48
+ * Whether a decision on `(source, outcome)` leaves the ref's next attempt to
49
+ * its content instead of a clock (#998). An accepted or rejected consolidate
50
+ * promotion is a verdict on that memory's text; asking the model about the
51
+ * same text again can only reproduce the proposal (accepted used to be
52
+ * eligible at once, rejected after 7 days), so the memory waits for an edit.
53
+ * The clock stays for a row with no recorded hash — one decided before the
54
+ * hash was recorded — see {@link nextEligibleAt} and {@link isContentDrivenRow}.
55
+ */
56
+ export function isContentDrivenDecision(source, outcome) {
57
+ return source === CONSOLIDATE_LEDGER_SOURCE && (outcome === "accepted" || outcome === "rejected");
58
+ }
59
+ /**
60
+ * Whether this row is held by its content hash: a decided consolidate
61
+ * promotion that recorded the body it was decided against. Such a row has no
62
+ * `next_eligible_at`; the caller compares `contentHash` with the asset's
63
+ * current body hash (the pair pass does the same in `selectInitiators`).
64
+ */
65
+ export function isContentDrivenRow(row) {
66
+ return row.contentHash !== null && isContentDrivenDecision(row.source, row.outcome);
67
+ }
41
68
  /**
42
69
  * Outcomes whose window a fresh signal on the asset (new feedback, a content
43
70
  * change) cannot lift. Every other window is a revisit cadence that a signal
@@ -73,12 +100,17 @@ function windowDays(source, outcome) {
73
100
  }
74
101
  /**
75
102
  * The single cadence function: when a `(source, outcome)` recorded at
76
- * `fromIso` becomes eligible again, or `null` for "immediately".
103
+ * `fromIso` becomes eligible again, or `null` for "immediately". A decision
104
+ * that {@link isContentDrivenDecision} holds by content, recorded together
105
+ * with the body hash it was decided against, starts no clock at all: `null`
106
+ * here means the hash is the whole test, not that the ref is free to retry.
77
107
  */
78
- export function nextEligibleAt(source, outcome, fromIso) {
108
+ export function nextEligibleAt(source, outcome, fromIso, contentHash) {
79
109
  const from = Date.parse(fromIso);
80
110
  if (!Number.isFinite(from))
81
111
  return null;
112
+ if (contentHash && isContentDrivenDecision(source, outcome))
113
+ return null;
82
114
  const days = windowDays(source, outcome);
83
115
  return days === null ? null : new Date(from + days * MS_PER_DAY).toISOString();
84
116
  }
@@ -127,7 +159,7 @@ export function recordImproveLedger(db, input) {
127
159
  source: input.source,
128
160
  lastAttemptAt: input.at,
129
161
  outcome: input.outcome,
130
- nextEligibleAt: nextEligibleAt(input.source, input.outcome, input.at),
162
+ nextEligibleAt: nextEligibleAt(input.source, input.outcome, input.at, input.contentHash),
131
163
  proposalId: input.proposalId ?? null,
132
164
  detail: trimDetail(input.detail),
133
165
  contentHash: input.contentHash ?? null,
@@ -150,14 +182,16 @@ export function recordImproveLedger(db, input) {
150
182
  * differ from the ledger key (a distill proposal for `lessons/x` is keyed by
151
183
  * its input `memories/x`) — and `last_attempt_at` is kept: the window starts
152
184
  * at the decision, the attempt happened when it happened. A proposal no row
153
- * knows (minted before the ledger existed) gets a row keyed by its own ref.
185
+ * knows (minted before the ledger existed, or whose row a later `judged_no_action`
186
+ * overwrote) gets a row keyed by `input.ref`.
154
187
  */
155
188
  export function recordImproveLedgerDecision(db, input) {
189
+ const hash = isContentDrivenDecision(input.source, input.outcome) ? input.contentHash : undefined;
156
190
  const changes = db
157
191
  .prepare(`UPDATE improve_ledger
158
- SET outcome = ?, next_eligible_at = ?, detail = ?
192
+ SET outcome = ?, next_eligible_at = ?, detail = ?, content_hash = COALESCE(?, content_hash)
159
193
  WHERE stash_dir = ? AND proposal_id = ?`)
160
- .run(input.outcome, nextEligibleAt(input.source, input.outcome, input.at), trimDetail(input.detail), input.stashDir, input.proposalId).changes;
194
+ .run(input.outcome, nextEligibleAt(input.source, input.outcome, input.at, hash), trimDetail(input.detail), hash ?? null, input.stashDir, input.proposalId).changes;
161
195
  if (Number(changes) > 0)
162
196
  return;
163
197
  recordImproveLedger(db, {
@@ -168,8 +202,33 @@ export function recordImproveLedgerDecision(db, input) {
168
202
  at: input.at,
169
203
  proposalId: input.proposalId,
170
204
  ...(input.detail !== undefined ? { detail: input.detail } : {}),
205
+ ...(hash !== undefined ? { contentHash: hash } : {}),
171
206
  });
172
207
  }
208
+ /**
209
+ * A rejected proposal was reopened (`akm proposal reopen`): put the rows
210
+ * {@link recordImproveLedgerDecision} found by `proposal_id` back to what the
211
+ * mint wrote — `proposed`, on the revisit cadence from the reopen, and no
212
+ * `content_hash` (a mint records none) — so the rejection's hard window stops
213
+ * reporting (and blocking) a proposal that is pending again.
214
+ * `last_attempt_at` is kept, as a decision keeps it. A proposal with no such
215
+ * row (its mint wrote none, or a later attempt took the key over) changes
216
+ * nothing.
217
+ */
218
+ export function reopenImproveLedgerDecision(db, input) {
219
+ db.prepare(`UPDATE improve_ledger
220
+ SET outcome = 'proposed', next_eligible_at = ?, detail = ?, content_hash = NULL
221
+ WHERE stash_dir = ? AND proposal_id = ?`).run(nextEligibleAt(input.source, "proposed", input.at), trimDetail(input.detail), input.stashDir, input.proposalId);
222
+ }
223
+ /**
224
+ * The same reopen for a proposal whose mint deliberately wrote no ledger row (a
225
+ * retire proposal — see `createRetireProposal`): the row its rejection created
226
+ * is dropped, returning the ledger to what the mint left. Found by
227
+ * `proposal_id`, like {@link recordImproveLedgerDecision}.
228
+ */
229
+ export function forgetImproveLedgerDecision(db, stashDir, proposalId) {
230
+ db.prepare("DELETE FROM improve_ledger WHERE stash_dir = ? AND proposal_id = ?").run(stashDir, proposalId);
231
+ }
173
232
  /**
174
233
  * Should-fix 6 (second review round): a read-only or dry-run open never
175
234
  * migrates, so it can land on a state.db from before migration 029 added
@@ -80,9 +80,11 @@ export function upsertEmbedding(db, entryId, embedding, model) {
80
80
  * (ties by id): an exact scan of the current model's rows. `distance` is
81
81
  * `sqrt(2 * (1 - cosine))`, the L2 distance between the unit-normalised
82
82
  * vectors. Rows of another width, left by a model that is no longer current,
83
- * never match.
83
+ * never match. With a `scope`, only that bundle's entries of that type are
84
+ * scanned: the `k` nearest of those, not the `k` nearest of everything filtered
85
+ * afterwards.
84
86
  */
85
- export function searchVec(db, query, k) {
87
+ export function searchVec(db, query, k, scope) {
86
88
  const dim = query.length;
87
89
  const q = Float64Array.from(query);
88
90
  let queryNorm = 0;
@@ -92,11 +94,13 @@ export function searchVec(db, query, k) {
92
94
  return [];
93
95
  queryNorm = Math.sqrt(queryNorm);
94
96
  const current = modelPredicate(db, currentEmbeddingModel(db));
97
+ const scoped = scope ? " AND embeddings.id IN (SELECT id FROM entries WHERE type = ? AND bundle_id = ?)" : "";
98
+ const params = scope ? [...current.params, scope.type, scope.bundleId] : current.params;
95
99
  const ids = [];
96
100
  const similarities = [];
97
101
  const rows = db
98
- .prepare(`SELECT id, embedding FROM embeddings WHERE ${current.sql}`)
99
- .iterate(...current.params);
102
+ .prepare(`SELECT id, embedding FROM embeddings WHERE ${current.sql}${scoped}`)
103
+ .iterate(...params);
100
104
  for (const { id, embedding } of rows) {
101
105
  if (embedding.byteLength !== dim * 4)
102
106
  continue;
@@ -126,14 +130,15 @@ export function searchVec(db, query, k) {
126
130
  /**
127
131
  * The k nearest neighbours of an already-indexed entry, by its stored vector —
128
132
  * no re-embedding, no network. Returns [] when the entry has no stored
129
- * vector. The entry itself is typically returned with distance ~0; callers
130
- * filter it out by id.
133
+ * vector. Unscoped, the entry itself is typically returned with distance ~0;
134
+ * callers filter it out by id. A `scope` confines the neighbours to one
135
+ * bundle's entries of one type (see {@link searchVec}).
131
136
  */
132
- export function getNeighborsByEntryId(db, id, k) {
137
+ export function getNeighborsByEntryId(db, id, k, scope) {
133
138
  const row = db.prepare("SELECT embedding FROM embeddings WHERE id = ?").get(id);
134
139
  if (!row || row.embedding.byteLength % 4 !== 0)
135
140
  return [];
136
- return searchVec(db, new Float32Array(row.embedding.slice().buffer), k);
141
+ return searchVec(db, new Float32Array(row.embedding.slice().buffer), k, scope);
137
142
  }
138
143
  /**
139
144
  * Every entry that has no embedding row for `model` (any row when no model is