akm-cli 0.9.7 → 0.9.8-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/dist/commands/env/env.js +2 -21
  3. package/dist/commands/health/checks.js +0 -25
  4. package/dist/commands/health/data-dir-usage.js +165 -0
  5. package/dist/commands/health/improve-metrics.js +8 -34
  6. package/dist/commands/health/windows.js +0 -4
  7. package/dist/commands/health.js +18 -37
  8. package/dist/commands/improve/consolidate/eligibility.js +11 -5
  9. package/dist/commands/improve/extract.js +36 -32
  10. package/dist/commands/improve/memory/memory-belief.js +15 -5
  11. package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
  12. package/dist/commands/improve/memory/memory-improve.js +7 -19
  13. package/dist/commands/improve/preparation.js +52 -36
  14. package/dist/commands/improve/reflect.js +13 -17
  15. package/dist/commands/lint/base-linter.js +42 -2
  16. package/dist/commands/lint/index.js +4 -5
  17. package/dist/commands/migrate/config-extra-params.js +61 -0
  18. package/dist/commands/migrate/dead-residue.js +113 -0
  19. package/dist/commands/migrate/stale-txn.js +49 -0
  20. package/dist/commands/migrate-cli.js +42 -1
  21. package/dist/commands/proposal/proposal.js +1 -21
  22. package/dist/commands/proposal/repository.js +0 -4
  23. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
  24. package/dist/commands/read/curate.js +4 -18
  25. package/dist/commands/read/show.js +2 -1
  26. package/dist/commands/sources/installed-stashes.js +5 -1
  27. package/dist/commands/sources/self-update.js +38 -1
  28. package/dist/commands/sources/sources-cli.js +17 -1
  29. package/dist/commands/tasks/tasks.js +0 -8
  30. package/dist/commands/url-checker.js +20 -28
  31. package/dist/commands/workflow-cli.js +11 -61
  32. package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
  33. package/dist/core/adapter/adapters/akm-adapter.js +2 -3
  34. package/dist/core/adapter/adapters/akm-lint.js +2 -20
  35. package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
  36. package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
  37. package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
  38. package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
  39. package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
  40. package/dist/core/adapter/adapters/okf-adapter.js +7 -18
  41. package/dist/core/adapter/adapters/shared.js +2 -0
  42. package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
  43. package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
  44. package/dist/core/adapter/validate-context.js +1 -3
  45. package/dist/core/asset/asset-placement.js +1 -3
  46. package/dist/core/asset/frontmatter.js +96 -0
  47. package/dist/core/common.js +66 -2
  48. package/dist/core/config/config-io.js +21 -8
  49. package/dist/core/config/config-walker.js +6 -10
  50. package/dist/core/config/config.js +11 -8
  51. package/dist/core/extra-params.js +17 -13
  52. package/dist/core/improve-result.js +1 -3
  53. package/dist/core/json-schema.js +9 -11
  54. package/dist/core/state/migrations.js +74 -2
  55. package/dist/core/state-db.js +2 -1
  56. package/dist/core/warn.js +15 -0
  57. package/dist/execution/executable-identity.js +1 -3
  58. package/dist/execution/guarded-source.js +1 -6
  59. package/dist/indexer/bundle-identity-guard.js +6 -1
  60. package/dist/indexer/db/graph-db.js +139 -154
  61. package/dist/indexer/ensure-index.js +11 -19
  62. package/dist/indexer/graph/graph-boost.js +23 -34
  63. package/dist/indexer/graph/graph-extraction.js +12 -2
  64. package/dist/indexer/indexer.js +43 -22
  65. package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
  66. package/dist/indexer/passes/dir-staleness.js +30 -9
  67. package/dist/indexer/passes/memory-inference.js +7 -2
  68. package/dist/indexer/scan/drain-dir.js +2 -4
  69. package/dist/indexer/search/db-search.js +3 -3
  70. package/dist/indexer/search/fts-query.js +10 -15
  71. package/dist/indexer/search/search-source.js +0 -13
  72. package/dist/indexer/usage/usage-events.js +9 -1
  73. package/dist/indexer/walk/walker.js +11 -6
  74. package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
  75. package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
  76. package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
  77. package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
  78. package/dist/integrations/harnesses/index.js +0 -4
  79. package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
  80. package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
  81. package/dist/integrations/lockfile.js +0 -14
  82. package/dist/integrations/session-logs/index.js +0 -81
  83. package/dist/llm/client.js +0 -12
  84. package/dist/llm/memory-infer.js +1 -3
  85. package/dist/llm/usage-telemetry.js +1 -3
  86. package/dist/registry/create-provider-registry.js +4 -0
  87. package/dist/registry/factory.js +4 -0
  88. package/dist/registry/resolve.js +23 -8
  89. package/dist/runtime.js +0 -12
  90. package/dist/scripts/akm-migrate-node.js +179 -248
  91. package/dist/scripts/akm-migrate.js +179 -248
  92. package/dist/setup/setup.js +6 -12
  93. package/dist/sources/providers/git-install.js +7 -2
  94. package/dist/sources/providers/tar-utils.js +1 -7
  95. package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
  96. package/dist/storage/managed-db.js +20 -7
  97. package/dist/storage/repositories/events-repository.js +0 -81
  98. package/dist/storage/repositories/index-entries-repository.js +24 -28
  99. package/dist/storage/repositories/index-fts-repository.js +24 -30
  100. package/dist/storage/repositories/index-meta-repository.js +6 -4
  101. package/dist/storage/repositories/index-schema.js +16 -1
  102. package/dist/storage/repositories/index-utility-repository.js +0 -57
  103. package/dist/storage/repositories/index-vec-repository.js +25 -27
  104. package/dist/storage/repositories/proposals-repository.js +4 -1
  105. package/dist/storage/repositories/task-history-repository.js +9 -3
  106. package/dist/tasks/backends/launchd.js +8 -18
  107. package/dist/tasks/resolve-akm-bin.js +15 -0
  108. package/dist/tasks/run/task-history.js +21 -31
  109. package/dist/tasks/scheduler-sync.js +1 -6
  110. package/dist/tasks/source/bounded-document.js +1 -14
  111. package/dist/tasks/source/task-to-v3.js +10 -2
  112. package/dist/workflows/exec/child-workflow.js +1 -1
  113. package/dist/workflows/exec/native-executor.js +2 -2
  114. package/dist/workflows/exec/step-work.js +5 -17
  115. package/dist/workflows/exec/worktree.js +40 -6
  116. package/dist/workflows/freeze/task-bindings.js +2 -4
  117. package/dist/workflows/ir/compile.js +3 -14
  118. package/dist/workflows/ir/schema-v4.js +4 -6
  119. package/dist/workflows/ir/schema.js +2 -5
  120. package/dist/workflows/parser.js +23 -26
  121. package/dist/workflows/source-files.js +8 -13
  122. package/dist/workflows/source-ir/schema.js +1 -19
  123. package/docs/migration/v0.9.0-troubleshooting.md +12 -2
  124. package/docs/migration/v0.9.1-to-v0.9.2.md +18 -4
  125. package/docs/reference/cli.md +3 -1
  126. package/docs/reference/data-and-telemetry.md +1 -0
  127. package/docs/reference/tasks.md +11 -0
  128. package/docs/reference/workflow-schema.md +24 -0
  129. package/package.json +2 -9
  130. package/dist/workflows/source-ir/compare.js +0 -17
@@ -11,14 +11,13 @@ import { stashDirFor } from "../../core/asset/asset-placement.js";
11
11
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
12
12
  import { conceptIdForStashFile, displayRefForConceptId } from "../../core/asset/resolve-ref.js";
13
13
  import { deriveBundleIds } from "../../core/bundle-id.js";
14
- import { isAkmRegistryCachePath, resolveStashDir } from "../../core/common.js";
14
+ import { compareCodePoints, isAkmRegistryCachePath, resolveStashDir } from "../../core/common.js";
15
15
  import { loadConfig, primaryBundlePath } from "../../core/config/config.js";
16
16
  import { UsageError } from "../../core/errors.js";
17
17
  import { warn } from "../../core/warn.js";
18
18
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
19
19
  import { TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail } from "../../tasks/source-v3.js";
20
20
  import { resolveWorkflowSourceDomains } from "../../workflows/source-files.js";
21
- import { compareWorkflowSourceCodePoints } from "../../workflows/source-ir/compare.js";
22
21
  import { runBaseChecks } from "./base-linter.js";
23
22
  import { checkEnvForDangerousKeys } from "./env-key-rules.js";
24
23
  import { isAdvisoryLintIssue } from "./types.js";
@@ -87,7 +86,7 @@ function collectWorkflowFiles(dir) {
87
86
  const results = [];
88
87
  for (const entry of fs
89
88
  .readdirSync(dir, { withFileTypes: true })
90
- .sort((left, right) => compareWorkflowSourceCodePoints(left.name, right.name))) {
89
+ .sort((left, right) => compareCodePoints(left.name, right.name))) {
91
90
  if (entry.name === ".git")
92
91
  continue;
93
92
  const full = path.join(dir, entry.name);
@@ -103,7 +102,7 @@ function collectWorkflowFiles(dir) {
103
102
  if (extension === ".md" || extension === ".yml")
104
103
  results.push(full);
105
104
  }
106
- return results.sort(compareWorkflowSourceCodePoints);
105
+ return results.sort(compareCodePoints);
107
106
  }
108
107
  /**
109
108
  * Resolve the already-enumerated candidates through the same workflow-source
@@ -131,7 +130,7 @@ function resolveWorkflowLintOwnership(stashRoot, files) {
131
130
  fixed: false,
132
131
  });
133
132
  }
134
- return { files: ownedFiles.sort(compareWorkflowSourceCodePoints), issues };
133
+ return { files: ownedFiles.sort(compareCodePoints), issues };
135
134
  }
136
135
  // ── Non-akm adapter dispatch (real `adapter.validate()`, not a re-implementation) ──
137
136
  //
@@ -0,0 +1,61 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Legacy `extraParams` -> first-class-field lift (#852, following #815), as
6
+ * an `akm migrate` concern.
7
+ *
8
+ * This used to run silently, in memory, on every config load
9
+ * (`liftLegacyEngineExtraParams` called from `parseAndValidateConfigText`)
10
+ * and never wrote the result back — so the lift, and its warning, recurred
11
+ * forever. config.json is akm-owned plain JSON; the lift is deterministic
12
+ * and total, so — like the dead-residue cleanup in `./dead-residue.ts` —
13
+ * it belongs in `akm migrate`, run once, persisted. `parseAndValidateConfigText`
14
+ * now fails closed on an unmigrated config instead of lifting it.
15
+ */
16
+ import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, writeConfigAtomic, } from "../../core/config/config-io.js";
17
+ import { liftLegacyEngineExtraParams } from "../../core/extra-params.js";
18
+ function readRawConfig(configPath) {
19
+ const text = readConfigText(configPath);
20
+ if (text === undefined)
21
+ return undefined;
22
+ return parseConfigText(text, configPath);
23
+ }
24
+ /**
25
+ * Read-only: what `akm migrate apply` would lift (or flag as conflicting) in
26
+ * `config.json`'s `extraParams`. Never touches disk. Returns an empty plan
27
+ * when the config file does not exist.
28
+ */
29
+ export function findConfigExtraParamsLift(configPath) {
30
+ const raw = readRawConfig(configPath);
31
+ if (!raw)
32
+ return { lifted: [], conflicts: [] };
33
+ const { lifted, conflicts } = liftLegacyEngineExtraParams(raw);
34
+ return { lifted, conflicts };
35
+ }
36
+ /**
37
+ * Persist the legacy extraParams -> first-class field lift to `config.json`,
38
+ * once. Best-effort like `removeDeadResidue`: a genuine conflict (extraParams
39
+ * and the first-class field set to different values) is left untouched here
40
+ * — `parseAndValidateConfigText` already hard-rejects that config at every
41
+ * load with the exact mismatch, so a second, weaker error here would only be
42
+ * noise — and is reported back via `conflicts` instead.
43
+ */
44
+ export function applyConfigExtraParamsLift(configPath) {
45
+ const raw = readRawConfig(configPath);
46
+ if (!raw)
47
+ return { applied: false, lifted: [], conflicts: [] };
48
+ const { config, lifted, conflicts } = liftLegacyEngineExtraParams(raw);
49
+ if (conflicts.length > 0 || lifted.length === 0) {
50
+ return { applied: false, lifted, conflicts };
51
+ }
52
+ const release = acquireConfigLock();
53
+ try {
54
+ backupExistingConfig(configPath);
55
+ writeConfigAtomic(configPath, config);
56
+ }
57
+ finally {
58
+ release();
59
+ }
60
+ return { applied: true, lifted, conflicts: [] };
61
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Dead pre-0.9.0 `$STASH/.akm` residue: superseded filesystem layouts with no
3
+ * live reader or writer anywhere in src/ (each verified by grep before being
4
+ * listed — see #889 for the audit). The largest, `.akm/proposals/`, was
5
+ * replaced by the `proposals` table in state.db in 0.9.0 and measured 135 MB
6
+ * on a real bundle.
7
+ *
8
+ * This lives under `migrate/` because removing a superseded layout IS
9
+ * migration — the tail end of the moves that created these paths' replacements.
10
+ * It was first shipped as a bolted-on `akm health --clean-dead-residue` flag
11
+ * plus a health advisory; that was the wrong shape (a special-purpose switch
12
+ * apologizing for migrations that did not finish their own job) and was
13
+ * removed. `akm migrate status` reports what is here; `akm migrate apply`
14
+ * removes it, exactly as it applies every other pending migration.
15
+ */
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ const DEAD_RESIDUE_PATHS = [
19
+ { name: "proposals", reason: "superseded by the `proposals` table in $DATA/state.db (0.9.0)" },
20
+ { prefix: "runs.archived-", reason: "orphaned archive of a directory that no longer exists" },
21
+ {
22
+ name: "archive",
23
+ reason: "legacy consolidation archive; current prune writes .akm/memory-cleanup/archive/ instead",
24
+ },
25
+ { name: "graph.json", reason: "superseded by the graph_* tables in $DATA/index.db" },
26
+ { name: "consolidate-journal.json", reason: "legacy consolidation journal; unused" },
27
+ { name: "proposals.db", reason: "empty legacy database file" },
28
+ { name: "mv-transactions", reason: "legacy fs-txn journal location; unused" },
29
+ ];
30
+ function dirSizeBytes(target) {
31
+ let total = 0;
32
+ let entries;
33
+ try {
34
+ entries = fs.readdirSync(target, { withFileTypes: true });
35
+ }
36
+ catch {
37
+ return 0;
38
+ }
39
+ for (const entry of entries) {
40
+ const entryPath = path.join(target, entry.name);
41
+ if (entry.isDirectory()) {
42
+ total += dirSizeBytes(entryPath);
43
+ }
44
+ else if (entry.isFile()) {
45
+ try {
46
+ total += fs.statSync(entryPath).size;
47
+ }
48
+ catch {
49
+ // Skip files that vanish or are inaccessible between readdir and stat.
50
+ }
51
+ }
52
+ }
53
+ return total;
54
+ }
55
+ function sizeOf(target) {
56
+ const st = fs.statSync(target);
57
+ return st.isDirectory() ? dirSizeBytes(target) : st.size;
58
+ }
59
+ /**
60
+ * Find every Tier-1 dead-residue path that actually exists under
61
+ * `$STASH/.akm`, with its computed size. Read-only — never deletes.
62
+ */
63
+ export function findDeadResidueEntries(stashDir) {
64
+ const akmDir = path.join(stashDir, ".akm");
65
+ let names;
66
+ try {
67
+ names = fs.readdirSync(akmDir);
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ const found = [];
73
+ for (const spec of DEAD_RESIDUE_PATHS) {
74
+ const matches = spec.name !== undefined ? [spec.name] : names.filter((n) => n.startsWith(spec.prefix ?? "\0"));
75
+ for (const name of matches) {
76
+ if (!names.includes(name))
77
+ continue;
78
+ const absolutePath = path.join(akmDir, name);
79
+ let sizeBytes;
80
+ try {
81
+ sizeBytes = sizeOf(absolutePath);
82
+ }
83
+ catch {
84
+ continue; // vanished between readdir and stat
85
+ }
86
+ found.push({ relativePath: path.join(".akm", name), absolutePath, sizeBytes, reason: spec.reason });
87
+ }
88
+ }
89
+ return found;
90
+ }
91
+ /**
92
+ * Delete every Tier-1 dead-residue path found under `$STASH/.akm`. Only
93
+ * invoked when the caller has explicitly opted in (`akm health
94
+ * --clean-dead-residue`) — never as a side effect of a plain `akm health`
95
+ * read. Best-effort per-path: one failure does not abort the rest.
96
+ */
97
+ export function removeDeadResidue(stashDir) {
98
+ const entries = findDeadResidueEntries(stashDir);
99
+ return entries.map((entry) => {
100
+ try {
101
+ fs.rmSync(entry.absolutePath, { recursive: true, force: true });
102
+ return { relativePath: entry.relativePath, sizeBytes: entry.sizeBytes, removed: true };
103
+ }
104
+ catch (error) {
105
+ return {
106
+ relativePath: entry.relativePath,
107
+ sizeBytes: entry.sizeBytes,
108
+ removed: false,
109
+ error: error instanceof Error ? error.message : String(error),
110
+ };
111
+ }
112
+ });
113
+ }
@@ -0,0 +1,49 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Stale durable-transaction journals (`$DATA/txn/<rootNs24>/<id>/journal.json`,
6
+ * see `core/fs-txn.ts`) that never got recovered — a crash mid-transaction
7
+ * left a journal on disk with no process left to finish or roll it back.
8
+ * `akm health` used to only report these and point at a troubleshooting doc
9
+ * (issue: an advisory that tells the user to go read docs instead of the
10
+ * tool recovering its own interrupted state). Recovery IS a migration
11
+ * concern like dead `.akm/` residue: `akm migrate status` names what is
12
+ * here for the stash root, `akm migrate apply` recovers it, exactly like
13
+ * `dead-residue.ts`.
14
+ *
15
+ * Recovery goes through {@link recoverTxnsForRoot}, which requires every
16
+ * live kind's registrar to be IMPORTED first so its handler is registered
17
+ * (see fs-txn.ts's module docs). `proposal/repository.ts` registers the
18
+ * `proposal`/`proposal-reject` kinds specifically so "ANY recovery entry
19
+ * point ... can finish or roll back an interrupted proposal mutation for a
20
+ * root it touches" (its own comment on the registration) — this module is
21
+ * exactly that kind of entry point.
22
+ */
23
+ import { canonicalTxnRoot, listTxnJournalsTolerant, recoverTxnsForRoot } from "../../core/fs-txn.js";
24
+ // Side-effect import: registers the `proposal`/`proposal-reject` txn kinds
25
+ // so recovery below can roll them forward/back for the stash root.
26
+ import "../proposal/repository.js";
27
+ /**
28
+ * Find every durable-transaction journal bound to `stashDir`'s namespace.
29
+ * Read-only, tolerant of a corrupt journal (counted, not thrown on) — mirrors
30
+ * `findDeadResidueEntries`'s read-only/never-mutates contract.
31
+ */
32
+ export function findStaleTxnEntries(stashDir) {
33
+ const root = canonicalTxnRoot(stashDir);
34
+ const { matches } = listTxnJournalsTolerant((j) => canonicalTxnRoot(j.root) === root);
35
+ return matches.map(({ journal }) => journalToEntry(journal));
36
+ }
37
+ function journalToEntry(journal) {
38
+ return { transactionId: journal.transactionId, kind: journal.kind, phase: journal.phase, root: journal.root };
39
+ }
40
+ /**
41
+ * Recover every durable transaction bound to `stashDir`'s namespace: roll
42
+ * back journals before their kind's commit point, roll forward the rest.
43
+ * The counterpart to {@link findStaleTxnEntries}, invoked only from `akm
44
+ * migrate apply`.
45
+ */
46
+ export async function recoverStaleTxns(stashDir) {
47
+ const recovered = await recoverTxnsForRoot(stashDir);
48
+ return recovered.map(journalToEntry);
49
+ }
@@ -2,6 +2,12 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
5
+ import { resolveStashDir } from "../core/common.js";
6
+ import { ConfigError } from "../core/errors.js";
7
+ import { getConfigPath } from "../core/paths.js";
8
+ import { applyConfigExtraParamsLift, findConfigExtraParamsLift } from "./migrate/config-extra-params.js";
9
+ import { findDeadResidueEntries, removeDeadResidue } from "./migrate/dead-residue.js";
10
+ import { findStaleTxnEntries, recoverStaleTxns } from "./migrate/stale-txn.js";
5
11
  import { runMigrationTool } from "./migration-tool.js";
6
12
  async function callMigrateTool(args, runTool) {
7
13
  const result = await runTool(args);
@@ -79,6 +85,25 @@ export function resolveGenerationStatus(call, label) {
79
85
  * got to look at a stable tree in that case.
80
86
  */
81
87
  export async function runMigrateSubcommand(command, genOneArgs, genTwoArgs, runTool = runMigrationTool) {
88
+ // Superseded pre-0.9.0 .akm layouts are a migration concern like any other:
89
+ // status names them, apply removes them. (First shipped as a bolted-on
90
+ // `health --clean-dead-residue` flag; folded here where it belongs.) The
91
+ // legacy extraParams -> first-class-field config lift (#852) is the same
92
+ // shape: status names it, apply persists it once instead of the old
93
+ // permanent silent lift on every config load.
94
+ // No configured bundle means there is no stash to scan — an empty domain,
95
+ // not an error — so migrate still works before `akm bundle create`. Any
96
+ // OTHER ConfigError propagates.
97
+ let stashDir;
98
+ try {
99
+ stashDir = resolveStashDir();
100
+ }
101
+ catch (error) {
102
+ if (!(error instanceof ConfigError) || error.code !== "STASH_DIR_NOT_FOUND")
103
+ throw error;
104
+ }
105
+ const configPath = getConfigPath();
106
+ const applyResidue = command === "migrate-apply" && !genOneArgs.includes("--dry-run");
82
107
  const first = await callMigrateTool(genOneArgs, runTool);
83
108
  if (first.status !== EXIT_CODES.SUCCESS && first.status !== EXIT_CODES.GENERAL) {
84
109
  process.exitCode = first.status;
@@ -95,7 +120,23 @@ export async function runMigrateSubcommand(command, genOneArgs, genTwoArgs, runT
95
120
  return;
96
121
  }
97
122
  const combined = combineMigrationPlans(first, second);
98
- output(command, combined);
123
+ // The stash-scoped sections need a bundle to scan; no configured bundle is
124
+ // an empty domain, not an error, so migrate still works before
125
+ // `akm bundle create`. The config lift is config-scoped and always runs.
126
+ const stashSections = stashDir === undefined
127
+ ? {}
128
+ : {
129
+ deadResidue: applyResidue
130
+ ? { removed: removeDeadResidue(stashDir) }
131
+ : { pending: findDeadResidueEntries(stashDir) },
132
+ staleTxns: applyResidue
133
+ ? { recovered: await recoverStaleTxns(stashDir) }
134
+ : { pending: findStaleTxnEntries(stashDir) },
135
+ };
136
+ const configExtraParams = applyResidue
137
+ ? applyConfigExtraParamsLift(configPath)
138
+ : { pending: findConfigExtraParamsLift(configPath) };
139
+ output(command, { ...combined, ...stashSections, configExtraParams });
99
140
  if (combined.status === "blocked")
100
141
  process.exitCode = EXIT_CODES.GENERAL;
101
142
  }
@@ -15,7 +15,7 @@ import { resolveStashDir } from "../../core/common.js";
15
15
  import { loadConfig } from "../../core/config/config.js";
16
16
  import { resolveWriteTarget } from "../../core/write-source.js";
17
17
  import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
18
- import { createProposal, diffProposal, isProposalSkipped, listProposals, preflightProposalPromotion, promoteProposal, proposalContent, recoverProposalTransactionsForStash, rejectProposalDurably, resolveProposalId, revertProposal, } from "./repository.js";
18
+ import { diffProposal, listProposals, preflightProposalPromotion, promoteProposal, proposalContent, recoverProposalTransactionsForStash, rejectProposalDurably, resolveProposalId, revertProposal, } from "./repository.js";
19
19
  import { validateProposal } from "./validators/proposals.js";
20
20
  // ── Shared helpers ──────────────────────────────────────────────────────────
21
21
  function resolveStash(stashDir) {
@@ -131,26 +131,6 @@ export function akmProposalDiff(options) {
131
131
  ...(diff.targetPath ? { targetPath: diff.targetPath } : {}),
132
132
  };
133
133
  }
134
- export function akmProposalCreate(options) {
135
- const queue = resolveProposalQueue(options.stashDir, options.queue, options.config);
136
- const stash = queue.stashDir;
137
- const target = options.target ?? (queue.target ? { source: queue.target.source.name, root: queue.target.source.path } : undefined);
138
- // Manual proposal creation (via `akm proposal create`) always bypasses
139
- // dedup/cooldown guards — the operator is explicitly requesting a proposal.
140
- const result = createProposal(stash, {
141
- ref: options.ref,
142
- source: options.source,
143
- ...(target !== undefined ? { target } : {}),
144
- ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
145
- payload: options.payload,
146
- force: true,
147
- }, options.ctx);
148
- if (isProposalSkipped(result)) {
149
- // Should never happen with force:true — defensive only.
150
- throw new Error(`Unexpected proposal skip: ${result.message}`);
151
- }
152
- return { schemaVersion: 1, ok: true, proposal: result };
153
- }
154
134
  /**
155
135
  * Restore an accepted proposal's prior content from the backup captured at
156
136
  * promotion time (Advantage D6c / Phase 6C).
@@ -563,9 +563,6 @@ export function listProposalsReadOnly(stashDir, options = {}, ctx) {
563
563
  }
564
564
  });
565
565
  }
566
- catch {
567
- return [];
568
- }
569
566
  finally {
570
567
  db?.close();
571
568
  }
@@ -1013,7 +1010,6 @@ async function finalizeProposalTransaction(txn, target, proposal, ctx) {
1013
1010
  paths: [commitPath],
1014
1011
  snapshots: p.gitSnapshots ?? {},
1015
1012
  onCommitRecorded: (commit) => {
1016
- // biome-ignore lint/style/noNonNullAssertion: publishWriteTargetTransaction throws when absent
1017
1013
  const publication = p.gitPublication;
1018
1014
  if (publication.commit !== commit) {
1019
1015
  publication.commit = commit;
@@ -190,12 +190,6 @@ export function hasHotCaptureMode(frontmatter) {
190
190
  return frontmatter?.captureMode === "hot";
191
191
  }
192
192
  // ── Consolidate merge size gate ──────────────────────────────────────────────
193
- /**
194
- * Ratio lower-bound for merged body vs. the larger source body.
195
- * Lower than reflect (0.5) because deduplication is expected — two memories
196
- * with 80-90% overlap legitimately compress to well under 50% of the larger.
197
- */
198
- export const MERGE_SHRINK_RATIO_MIN = 0.3;
199
193
  // ── Reflect size gate ────────────────────────────────────────────────────────
200
194
  /** Ratio lower-bound: proposed body must be at least this fraction of source. */
201
195
  export const REFLECT_SHRINK_RATIO_MIN = 0.5;
@@ -52,7 +52,9 @@ export const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
52
52
  export const MIN_CURATE_SEARCH_LIMIT = 12;
53
53
  const DEFAULT_CURATE_LIMIT = 4;
54
54
  const CURATE_CLOSE_SCORE_BAND = 0.12;
55
- const CURATE_TAIL_SCORE_FLOOR = 0.35;
55
+ // Used by `shouldRunCurateFallback` to judge whether the initial search
56
+ // already returned strong results (a different purpose from ranking/keeping
57
+ // hits — see that function for its own rationale).
56
58
  const CURATE_RELATIVE_SCORE_FLOOR = 0.7;
57
59
  const CURATE_FALLBACK_TOP_SCORE_THRESHOLD = 0.8;
58
60
  const CURATE_FALLBACK_STRONG_SCORE_FLOOR = 0.35;
@@ -577,11 +579,6 @@ function compareCurateHits(a, b) {
577
579
  return rawDiff;
578
580
  return a.originalIndex - b.originalIndex;
579
581
  }
580
- function passesCurateScoreFloor(hit, leaderScore) {
581
- if (leaderScore === undefined)
582
- return true;
583
- return hit.rawScore >= Math.max(CURATE_TAIL_SCORE_FLOOR, leaderScore * CURATE_RELATIVE_SCORE_FLOOR);
584
- }
585
582
  function isNarrowReferenceFamilyQuery(query, family) {
586
583
  if (!family || family.role !== "reference")
587
584
  return false;
@@ -602,19 +599,8 @@ function selectCuratedStashHits(query, hits, limit) {
602
599
  const ranked = collapsed.hits
603
600
  .map(({ hit, originalIndex }) => annotateCurateHit(query, hit, originalIndex, intent))
604
601
  .sort(compareCurateHits);
605
- const selected = [];
606
602
  const supportRefsByRef = collapsed.supportRefsByRef;
607
- let leaderScore;
608
- for (const candidate of ranked) {
609
- if (!passesCurateScoreFloor(candidate, leaderScore))
610
- continue;
611
- selected.push(candidate);
612
- if (leaderScore === undefined)
613
- leaderScore = candidate.rawScore;
614
- if (selected.length >= limit)
615
- break;
616
- }
617
- return { selected: selected.map((entry) => entry.hit), supportRefsByRef };
603
+ return { selected: ranked.slice(0, limit).map((entry) => entry.hit), supportRefsByRef };
618
604
  }
619
605
  function collapseCurateFamilies(query, hits) {
620
606
  const passthrough = [];
@@ -27,7 +27,7 @@ import { displayRef, typeNameFromConceptId } from "../../core/asset/resolve-ref.
27
27
  import { META_DIR, parseMetaRef, readMetaFile } from "../../core/asset/stash-meta.js";
28
28
  import { asNonEmptyString, isWithin } from "../../core/common.js";
29
29
  import { getIndexPassConfig, loadConfig } from "../../core/config/config.js";
30
- import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
30
+ import { NotFoundError, rethrowIfDataDirUnreadable, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
31
31
  import { appendEvent } from "../../core/events.js";
32
32
  import { SCRIPT_EXTENSIONS } from "../../core/recognition-util.js";
33
33
  import { presentationFor } from "../../core/type-presentation.js";
@@ -292,6 +292,7 @@ export async function showLocal(input) {
292
292
  }
293
293
  catch (err) {
294
294
  rethrowIfTestIsolationError(err);
295
+ rethrowIfDataDirUnreadable(err);
295
296
  return { total: 0, hits: [] };
296
297
  }
297
298
  })(),
@@ -47,7 +47,11 @@ function canonicalJson(value) {
47
47
  if (value && typeof value === "object") {
48
48
  const entries = Object.entries(value)
49
49
  .filter(([, entry]) => entry !== undefined)
50
- .sort(([left], [right]) => left.localeCompare(right));
50
+ // Codepoint order, NOT localeCompare: this string feeds sha256Hex for the
51
+ // bundle-audit generation, and localeCompare is ICU/locale-dependent — the
52
+ // same object could hash differently on two machines. Matches the other
53
+ // canonicalJson implementations (workflows/ir/plan-hash.ts).
54
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
51
55
  return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
52
56
  }
53
57
  return JSON.stringify(value);
@@ -333,7 +333,7 @@ function runPostUpgradeTasks(akmBin, opts, upgradeState) {
333
333
  ok: false,
334
334
  skipped: opts.skip,
335
335
  message: `Upgrade completed, but the state schema was not prepared (${detail}). ` +
336
- "Preserve state.db and run `akm upgrade --force` before other AKM commands.",
336
+ "Preserve state.db and run `akm upgrade --state-only` before other AKM commands (the binary is already current).",
337
337
  };
338
338
  }
339
339
  const stateNote = stateUpgrade.safetyCopyPath ? ` Historical state safety copy: ${stateUpgrade.safetyCopyPath}.` : "";
@@ -520,3 +520,40 @@ export function getPackageManagerUpgradeCommand(installMethod, packageName = get
520
520
  }
521
521
  return undefined;
522
522
  }
523
+ /**
524
+ * Apply pending historical destructive state.db migrations WITHOUT installing a
525
+ * new akm — the body of `akm upgrade --state-only`.
526
+ *
527
+ * Migrations flagged `historical-destructive` are refused during an ordinary
528
+ * managed open: they need a verified sibling safety copy taken under the
529
+ * migration writer lock, and that is deliberate, so an unattended `akm index`
530
+ * can never quietly drop operator state.
531
+ *
532
+ * The bug this fixes is not the guard but its reachability (#895). The only
533
+ * code path that set `allowHistoricalDestructiveStateUpgrade` ran as a
534
+ * POST-INSTALL step of a real upgrade, so it sat behind an npm install. Where
535
+ * akm is installed globally by an image and the runtime user is unprivileged,
536
+ * that install fails EACCES and throws long before the migration is reached —
537
+ * leaving the documented remedy impossible to run and `akm index --full`
538
+ * permanently blocked. Nothing about the migration itself needs the network,
539
+ * root, or a new binary; it is local, offline, and already verified.
540
+ *
541
+ * The safety copy is NOT skipped here. This changes only who may ask for the
542
+ * migration, never what it does.
543
+ */
544
+ export function upgradeStateOnly(currentVersion, dependencies) {
545
+ const upgradeState = dependencies?.upgradeHistoricalStateDatabase ?? upgradeHistoricalStateDatabase;
546
+ const result = upgradeState();
547
+ return {
548
+ currentVersion,
549
+ newVersion: currentVersion,
550
+ upgraded: false,
551
+ installMethod: detectInstallMethod(),
552
+ message: result.upgraded
553
+ ? `Applied pending state.db migrations. Safety copy: ${result.safetyCopyPath}`
554
+ : "state.db is already current; no migration was needed",
555
+ stateUpgrade: result.safetyCopyPath
556
+ ? { applied: result.upgraded, safetyCopyPath: result.safetyCopyPath }
557
+ : { applied: result.upgraded },
558
+ };
559
+ }
@@ -33,7 +33,7 @@ import { UsageError } from "../../core/errors.js";
33
33
  import { appendEvent } from "../../core/events.js";
34
34
  import { resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
35
35
  import { pkgVersion } from "../../version.js";
36
- import { checkForUpdate, performUpgrade } from "./self-update.js";
36
+ import { checkForUpdate, performUpgrade, upgradeStateOnly } from "./self-update.js";
37
37
  import { akmClone } from "./source-clone.js";
38
38
  export const upgradeCommand = defineJsonCommand({
39
39
  meta: { name: "upgrade", description: "Upgrade akm to the latest release" },
@@ -45,8 +45,24 @@ export const upgradeCommand = defineJsonCommand({
45
45
  description: "Skip the post-upgrade index rebuild",
46
46
  default: false,
47
47
  },
48
+ "state-only": {
49
+ type: "boolean",
50
+ description: "Apply pending state.db migrations without installing a new akm",
51
+ default: false,
52
+ },
48
53
  },
49
54
  async run({ args }) {
55
+ // Applying a historical destructive state migration used to be reachable
56
+ // ONLY as a post-install step of a real upgrade, so an install akm cannot
57
+ // rewrite -- a global npm install owned by root, an image that ships the
58
+ // CLI -- had no route to it at all: the npm step fails EACCES and throws
59
+ // long before the migration runs (#895). The migration is a local,
60
+ // offline, already-verified operation; it does not need the network or a
61
+ // new binary, and coupling it to one was the bug.
62
+ if (args["state-only"]) {
63
+ output("upgrade", upgradeStateOnly(pkgVersion));
64
+ return;
65
+ }
50
66
  const check = await checkForUpdate(pkgVersion);
51
67
  if (args.check) {
52
68
  output("upgrade", check);
@@ -875,14 +875,6 @@ function warnIneligibleRebind(runtime, explicitRebind, warnings) {
875
875
  return;
876
876
  warnings.push(`--rebind bound scheduled tasks to an ineligible ${runtime.kind ?? "unknown"} invocation (${runtime.binding.join(" ")}); scheduled runs will invoke a mutable, unproven binary. Install akm via \`npm install --global akm-cli\` or a standalone release, then re-run \`akm task sync --rebind\`.`);
877
877
  }
878
- export function prepareSchedulerRuntime(explicitRebind, operation, deps = {}) {
879
- const invocation = (deps.resolveInvocation ?? resolveAkmInvocation)();
880
- if (!invocation.eligible && !explicitRebind) {
881
- throw new UsageError(`Refusing to ${operation} from an ineligible ${invocation.kind ?? "unknown"} invocation (${invocation.argv.join(" ")}).`, "INVALID_FLAG_VALUE", "npm-global ownership could not be verified. Run `npm install --global akm-cli` and use that launcher, use a standalone installation, or explicitly repeat the operation with --rebind.");
882
- }
883
- const contextPath = (deps.writeDescriptor ?? writeSchedulerContextDescriptor)(schedulerContextDescriptor());
884
- return { binding: invocation.argv, contextPath, eligible: invocation.eligible, kind: invocation.kind };
885
- }
886
878
  function groupInstalledBindings(entries, invocation) {
887
879
  const groups = new Map();
888
880
  for (const entry of entries) {
@@ -2,40 +2,32 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  const URL_RE = /https?:\/\/[^\s"'<>)\]]+/g;
5
- const TIMEOUT_MS = 5000;
6
- const MAX_URLS = 20;
5
+ /**
6
+ * Check every URL in `entries` and report the ones that are dead.
7
+ *
8
+ * No cap, no per-entry slice, no ceiling option. There used to be a
9
+ * `MAX_URLS = 20` plus an undocumented `slice(0, 3)` per entry, so this
10
+ * examined at most twenty links in a bundle holding thousands and reported
11
+ * success. The first attempt at fixing that kept the cap and layered a
12
+ * coverage report, a warning, and an override on top — a constraint plus more
13
+ * code to explain the constraint. Both are gone. It checks what you asked it
14
+ * to check.
15
+ *
16
+ * A request that fails, times out, or cannot resolve surfaces as a `DeadUrl`
17
+ * rather than being swallowed, so a network problem is visible instead of
18
+ * looking like a clean bill of health.
19
+ */
7
20
  export async function checkDeadUrls(_stashDir, entries) {
8
- const urlsToCheck = [];
9
- for (const entry of entries) {
10
- if (urlsToCheck.length >= MAX_URLS)
11
- break;
12
- const matches = entry.body.match(URL_RE) ?? [];
13
- for (const url of matches.slice(0, 3)) {
14
- urlsToCheck.push({ ref: entry.ref, url });
15
- if (urlsToCheck.length >= MAX_URLS)
16
- break;
17
- }
18
- }
21
+ const urlsToCheck = entries.flatMap((entry) => (entry.body.match(URL_RE) ?? []).map((url) => ({ ref: entry.ref, url })));
19
22
  const results = [];
20
23
  await Promise.allSettled(urlsToCheck.map(async ({ ref, url }) => {
21
24
  try {
22
- const controller = new AbortController();
23
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
24
- const res = await fetch(url, {
25
- method: "HEAD",
26
- signal: controller.signal,
27
- redirect: "follow",
28
- });
29
- clearTimeout(timer);
30
- if (res.status >= 400) {
25
+ const res = await fetch(url, { method: "HEAD", redirect: "follow" });
26
+ if (res.status >= 400)
31
27
  results.push({ ref, url, status: res.status });
32
- }
33
28
  }
34
- catch (e) {
35
- if (e.name === "AbortError") {
36
- results.push({ ref, url, status: "timeout" });
37
- }
38
- // network errors (ENOTFOUND etc.) — skip, don't report as dead
29
+ catch {
30
+ results.push({ ref, url, status: "error" });
39
31
  }
40
32
  }));
41
33
  return results;