akm-cli 0.10.261004-beta → 0.10.261006-beta

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 (54) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/STABILITY.md +9 -0
  3. package/dist/assets/ui/app.js +91 -0
  4. package/dist/assets/ui/asset.js +67 -0
  5. package/dist/assets/ui/bundles.js +80 -0
  6. package/dist/assets/ui/changes.js +32 -0
  7. package/dist/assets/ui/charts.js +93 -0
  8. package/dist/assets/ui/config.js +69 -0
  9. package/dist/assets/ui/diff-view.js +202 -0
  10. package/dist/assets/ui/diff.js +261 -0
  11. package/dist/assets/ui/index.html +30 -0
  12. package/dist/assets/ui/lib.js +238 -0
  13. package/dist/assets/ui/markdown.js +103 -0
  14. package/dist/assets/ui/overview.js +145 -0
  15. package/dist/assets/ui/proposal-actions.js +273 -0
  16. package/dist/assets/ui/proposals.js +219 -0
  17. package/dist/assets/ui/search.js +62 -0
  18. package/dist/assets/ui/styles.css +1415 -0
  19. package/dist/cli.js +3 -0
  20. package/dist/commands/health/checks.js +5 -3
  21. package/dist/commands/health/egress.js +5 -1
  22. package/dist/commands/health/engine-usage.js +2 -2
  23. package/dist/commands/health/llm-usage.js +2 -2
  24. package/dist/commands/health/windows.js +5 -5
  25. package/dist/commands/health.js +40 -8
  26. package/dist/commands/improve/improve.js +17 -4
  27. package/dist/commands/improve/reflect-noise.js +4 -0
  28. package/dist/commands/improve/reflect.js +14 -5
  29. package/dist/commands/proposal/drain.js +13 -1
  30. package/dist/commands/proposal/proposal-cli.js +23 -6
  31. package/dist/commands/proposal/proposal.js +58 -11
  32. package/dist/commands/proposal/repository.js +67 -31
  33. package/dist/commands/read/search.js +2 -1
  34. package/dist/commands/read/show.js +8 -6
  35. package/dist/commands/ui/api.js +556 -0
  36. package/dist/commands/ui/git-read.js +101 -0
  37. package/dist/commands/ui/open-browser.js +30 -0
  38. package/dist/commands/ui/proposal-actions.js +179 -0
  39. package/dist/commands/ui/redact.js +88 -0
  40. package/dist/commands/ui/server.js +352 -0
  41. package/dist/commands/ui/ui-cli.js +62 -0
  42. package/dist/core/write-source.js +9 -5
  43. package/dist/indexer/search/db-search.js +2 -1
  44. package/dist/output/format-exempt.js +3 -0
  45. package/dist/sources/providers/git-stash.js +46 -15
  46. package/dist/storage/repositories/improve-runs-repository.js +6 -0
  47. package/dist/storage/repositories/metrics-repository.js +17 -0
  48. package/docs/architecture/0.10-process-rules.md +4 -5
  49. package/docs/architecture/improvement.md +9 -5
  50. package/docs/architecture/internals/improve-workflow.md +2 -2
  51. package/docs/reference/README.md +1 -0
  52. package/docs/reference/cli.md +62 -3
  53. package/docs/reference/ui-api.md +576 -0
  54. package/package.json +1 -1
@@ -436,34 +436,36 @@ function requireProposal(db, stashDir, id) {
436
436
  * proposal (then most recent archived); else by a unique pending id prefix.
437
437
  */
438
438
  export function resolveProposalId(stashDir, idOrRef, ctx) {
439
- return withProposalsDb(ctx, (db) => {
440
- const exact = getStateProposal(db, idOrRef, stashDir);
441
- if (exact)
442
- return exact;
443
- if (idOrRef.includes(":") || idOrRef.includes("/")) {
444
- const wantRef = filterRefIdentity(idOrRef);
445
- // Should-fix 7: by-ref resolution never picks a retire proposal — the
446
- // newest pending proposal for a ref could be a `consolidate-pair`
447
- // retirement rather than the reflect/distill edit a person typed the
448
- // ref to accept, and accepting it archives the asset instead. A retire
449
- // is reached by its own proposal id, or by the explicit generator
450
- // `consolidate-pair` (bulk accept/reject).
451
- const newest = (status) => listStateProposals(db, { stashDir, ...(status !== undefined ? { status } : {}) })
452
- .filter((p) => proposalMatchesRef(p.ref, wantRef) && !isRetireProposal(p))
453
- .sort((a, b) => new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime())[0];
454
- const found = newest("pending") ?? newest();
455
- if (found)
456
- return found;
457
- throw new NotFoundError(`No proposal found for ref "${idOrRef}".`, "PROPOSAL_NOT_FOUND");
458
- }
459
- const prefixMatches = listStateProposalIdsByPrefix(db, stashDir, idOrRef);
460
- if (prefixMatches.length === 1)
461
- return requireProposal(db, stashDir, prefixMatches[0]);
462
- if (prefixMatches.length > 1) {
463
- throw new UsageError(`Ambiguous prefix "${idOrRef}" — matches: ${prefixMatches.join(", ")}`, "INVALID_FLAG_VALUE");
464
- }
465
- throw new NotFoundError(`Proposal "${idOrRef}" not found.`, "PROPOSAL_NOT_FOUND");
466
- });
439
+ return withProposalsDb(ctx, (db) => resolveProposalIdIn(db, stashDir, idOrRef));
440
+ }
441
+ /** {@link resolveProposalId} over a connection the caller holds (`akm ui` reads state.db read-only). */
442
+ export function resolveProposalIdIn(db, stashDir, idOrRef) {
443
+ const exact = getStateProposal(db, idOrRef, stashDir);
444
+ if (exact)
445
+ return exact;
446
+ if (idOrRef.includes(":") || idOrRef.includes("/")) {
447
+ const wantRef = filterRefIdentity(idOrRef);
448
+ // Should-fix 7: by-ref resolution never picks a retire proposal — the
449
+ // newest pending proposal for a ref could be a `consolidate-pair`
450
+ // retirement rather than the reflect/distill edit a person typed the
451
+ // ref to accept, and accepting it archives the asset instead. A retire
452
+ // is reached by its own proposal id, or by the explicit generator
453
+ // `consolidate-pair` (bulk accept/reject).
454
+ const newest = (status) => listStateProposals(db, { stashDir, ...(status !== undefined ? { status } : {}) })
455
+ .filter((p) => proposalMatchesRef(p.ref, wantRef) && !isRetireProposal(p))
456
+ .sort((a, b) => new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime())[0];
457
+ const found = newest("pending") ?? newest();
458
+ if (found)
459
+ return found;
460
+ throw new NotFoundError(`No proposal found for ref "${idOrRef}".`, "PROPOSAL_NOT_FOUND");
461
+ }
462
+ const prefixMatches = listStateProposalIdsByPrefix(db, stashDir, idOrRef);
463
+ if (prefixMatches.length === 1)
464
+ return requireProposal(db, stashDir, prefixMatches[0]);
465
+ if (prefixMatches.length > 1) {
466
+ throw new UsageError(`Ambiguous prefix "${idOrRef}" — matches: ${prefixMatches.join(", ")}`, "INVALID_FLAG_VALUE");
467
+ }
468
+ throw new NotFoundError(`Proposal "${idOrRef}" not found.`, "PROPOSAL_NOT_FOUND");
467
469
  }
468
470
  /**
469
471
  * The ledger outcome of a decision. A procedural refusal (retention expiry, a
@@ -1060,6 +1062,37 @@ function requireAcceptedTarget(proposal) {
1060
1062
  function acceptCommitMessage(proposal) {
1061
1063
  return `akm accept: ${proposal.source} ${proposal.id.slice(0, 8)} ${proposal.ref}`;
1062
1064
  }
1065
+ function commitAccept(collector, target, proposal, paths) {
1066
+ if (collector)
1067
+ collector.push({ target, proposal, paths });
1068
+ else
1069
+ commitAcceptedPaths(target, acceptCommitMessage(proposal), paths);
1070
+ }
1071
+ /**
1072
+ * Commit everything `collector` holds, one commit per target repository: a lone
1073
+ * accept keeps its own subject; several get `<label>: N accepted (source count,
1074
+ * ...)` with one `<id8> <source> <ref>` body line per proposal. Empties the
1075
+ * collector. A failed commit only warns, as for a single accept.
1076
+ */
1077
+ export function commitCollectedAccepts(collector, label) {
1078
+ const byRepo = new Map();
1079
+ for (const entry of collector.splice(0)) {
1080
+ const key = path.resolve(entry.target.source.repoPath ?? entry.target.source.path);
1081
+ byRepo.set(key, [...(byRepo.get(key) ?? []), entry]);
1082
+ }
1083
+ for (const entries of byRepo.values()) {
1084
+ const first = entries[0];
1085
+ if (entries.length === 1) {
1086
+ commitAcceptedPaths(first.target, acceptCommitMessage(first.proposal), first.paths);
1087
+ continue;
1088
+ }
1089
+ const counts = new Map();
1090
+ for (const { proposal } of entries)
1091
+ counts.set(proposal.source, (counts.get(proposal.source) ?? 0) + 1);
1092
+ const bySource = [...counts].sort((a, b) => b[1] - a[1]).map(([source, count]) => `${source} ${count}`);
1093
+ commitAcceptedPaths(first.target, `${label}: ${entries.length} accepted (${bySource.join(", ")})`, entries.flatMap((entry) => entry.paths), entries.map(({ proposal }) => `${proposal.id.slice(0, 8)} ${proposal.source} ${proposal.ref}`));
1094
+ }
1095
+ }
1063
1096
  /**
1064
1097
  * O1 (alpha.9): an accepted consolidate PROMOTION retires its source memory
1065
1098
  * (and its `.derived` twin), so promotion no longer leaves a memory/
@@ -1163,7 +1196,7 @@ async function promoteProposalWithLease(stashDir, config, id, options, ctx) {
1163
1196
  const paths = [assetPath];
1164
1197
  if (accepted.status === "accepted" && accepted.source === "consolidate")
1165
1198
  retirePromotionSource(mutationTarget, accepted, paths);
1166
- commitAcceptedPaths(mutationTarget, acceptCommitMessage(accepted), paths);
1199
+ commitAccept(options.commitCollector, mutationTarget, accepted, paths);
1167
1200
  return { proposal: accepted, assetPath, ref: accepted.ref };
1168
1201
  }
1169
1202
  /**
@@ -1422,7 +1455,7 @@ async function retireProposalWithLease(stashDir, config, proposal, options, ctx)
1422
1455
  paths.push(twinPath, path.join(mutationTarget.source.path, twinRecord.archivedPath), path.join(mutationTarget.source.path, twinRecord.auditPath));
1423
1456
  }
1424
1457
  if (paths.length > 0)
1425
- commitAcceptedPaths(mutationTarget, acceptCommitMessage(proposal), paths);
1458
+ commitAccept(options.commitCollector, mutationTarget, proposal, paths);
1426
1459
  if (archiveDirs.length === 0) {
1427
1460
  // Recorded intent, but neither file is at its original location NOR
1428
1461
  // archived under this proposal's id: something else removed the target
@@ -1838,7 +1871,10 @@ export function reopenProposals(stashDir, config, ids, options = {}, ctx) {
1838
1871
  }
1839
1872
  /** The proposal against the asset its accept would overwrite (same target resolution as accept). */
1840
1873
  export function diffProposal(stashDir, config, id, options = {}, ctx) {
1841
- const proposal = getProposal(stashDir, id, ctx);
1874
+ return diffLoadedProposal(config, getProposal(stashDir, id, ctx), options);
1875
+ }
1876
+ /** {@link diffProposal} for a proposal the caller already read (`akm ui` reads state.db read-only). */
1877
+ export function diffLoadedProposal(config, proposal, options = {}) {
1842
1878
  const target = resolveProposalWriteTarget(config, proposal, options.target, options.queueTarget);
1843
1879
  const targetPath = resolveAssetFilePathSafe(target.source, parseRefInput(proposal.ref));
1844
1880
  const existing = targetPath && fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : null;
@@ -123,6 +123,7 @@ export async function akmSearch(input) {
123
123
  // would leak hits from sources the caller did not request.
124
124
  restrictToSources: namedSourceName !== undefined,
125
125
  includeExcludedTypes: input.includeSessions === true,
126
+ readOnly: input.readOnly,
126
127
  });
127
128
  const registryResult = source === "local"
128
129
  ? undefined
@@ -201,7 +202,7 @@ function usageSearchMode(mode) {
201
202
  return mode === "semantic" ? "semantic" : "keyword";
202
203
  }
203
204
  function maybeLogSearchEvent(input, query, response, mode) {
204
- if (input.skipLogging)
205
+ if (input.skipLogging || input.readOnly)
205
206
  return;
206
207
  logSearchEvent(query, response, mode, input.eventSource, input.attributionProjection);
207
208
  }
@@ -65,7 +65,7 @@ export async function akmShowUnified(input) {
65
65
  {
66
66
  const metaRef = parseMetaRef(ref);
67
67
  if (metaRef)
68
- return showStashMeta(metaRef);
68
+ return showStashMeta(metaRef, input.readOnly === true);
69
69
  }
70
70
  // Env/secret bodies have no safe fragment surface, and a fragment cannot
71
71
  // widen what the env/secret renderers expose: both always omit the body
@@ -77,7 +77,7 @@ export async function akmShowUnified(input) {
77
77
  // safe substrate below is its source of truth. Friendly heading selectors
78
78
  // intentionally retain the normal source-live read behavior.
79
79
  const parsedRef = parseBundleRef(ref);
80
- if (!parsedRef.fragment?.startsWith("akm-fragment-")) {
80
+ if (!input.readOnly && !parsedRef.fragment?.startsWith("akm-fragment-")) {
81
81
  const { primarySource } = resolveReadSources();
82
82
  await ensurePrimaryIndexForRead(primarySource);
83
83
  }
@@ -91,7 +91,7 @@ export async function akmShowUnified(input) {
91
91
  enforceScopeOrThrow(result.path, ref, input.scope);
92
92
  }
93
93
  // Count prior shows of this ref before logging the current one.
94
- if (!input.skipLogging) {
94
+ if (!input.skipLogging && !input.readOnly) {
95
95
  const consumedRef = result.ref ?? makeBundleRef(undefined, parseBundleRef(ref).conceptId);
96
96
  const priorShowCount = recentShowCount(consumedRef);
97
97
  recordShowUsage(consumedRef, result.type, result.name, input.eventSource, result.path);
@@ -110,7 +110,7 @@ export async function akmShowUnified(input) {
110
110
  * to that stash; an uninstalled origin yields an actionable "not installed"
111
111
  * error. The file is read directly from disk — `.meta/` is never indexed.
112
112
  */
113
- async function showStashMeta(metaRef) {
113
+ async function showStashMeta(metaRef, readOnly = false) {
114
114
  const allSources = resolveSourceEntries();
115
115
  const sources = resolveSourcesForOrigin(metaRef.origin, allSources);
116
116
  if (metaRef.origin && sources.length === 0) {
@@ -123,7 +123,9 @@ async function showStashMeta(metaRef) {
123
123
  if (!metaFile)
124
124
  continue;
125
125
  const editable = isEditable(metaFile.path, config, allSources);
126
- appendEvent({ eventType: "show", ref: `meta:${metaRef.name}`, metadata: { type: "meta", name: metaRef.name } });
126
+ if (!readOnly) {
127
+ appendEvent({ eventType: "show", ref: `meta:${metaRef.name}`, metadata: { type: "meta", name: metaRef.name } });
128
+ }
127
129
  return {
128
130
  type: "meta",
129
131
  name: metaRef.name,
@@ -366,7 +368,7 @@ export async function showLocal(input) {
366
368
  ...(!editable ? { editHint: buildEditHint(canonicalRef) } : {}),
367
369
  ...showLinks(indexedEntry.itemRef, displayDefaultBundle),
368
370
  };
369
- const activeRun = await getActiveWorkflowRun(getCurrentWorkflowScopeKey());
371
+ const activeRun = input.readOnly ? null : await getActiveWorkflowRun(getCurrentWorkflowScopeKey());
370
372
  if (activeRun) {
371
373
  fullResponse.activeRun = activeRun;
372
374
  }