akm-cli 0.9.0 → 0.9.1-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 (140) hide show
  1. package/CHANGELOG.md +724 -0
  2. package/README.md +28 -63
  3. package/STABILITY.md +4 -2
  4. package/dist/cli/parse-args.js +7 -1
  5. package/dist/commands/agent/contribute-cli.js +1 -1
  6. package/dist/commands/env/child-env.js +14 -0
  7. package/dist/commands/feedback-cli.js +7 -1
  8. package/dist/commands/health/llm-usage.js +2 -1
  9. package/dist/commands/health/surfaces.js +4 -77
  10. package/dist/commands/health.js +65 -11
  11. package/dist/commands/improve/distill/quality-gate.js +6 -1
  12. package/dist/commands/improve/eligibility.js +7 -1
  13. package/dist/commands/improve/eval-cases.js +2 -0
  14. package/dist/commands/improve/improve.js +126 -10
  15. package/dist/commands/improve/locks.js +7 -0
  16. package/dist/commands/improve/memory/memory-improve.js +9 -0
  17. package/dist/commands/improve/run-context.js +5 -0
  18. package/dist/commands/improve/session-asset.js +4 -0
  19. package/dist/commands/lint/base-linter.js +31 -7
  20. package/dist/commands/lint/index.js +205 -51
  21. package/dist/commands/lint/types.js +22 -1
  22. package/dist/commands/proposal/repository.js +17 -1
  23. package/dist/commands/sources/add-cli.js +8 -2
  24. package/dist/commands/sources/info.js +12 -2
  25. package/dist/commands/sources/installed-stashes.js +6 -1
  26. package/dist/commands/sources/migration-help.js +12 -3
  27. package/dist/commands/sources/self-update.js +9 -1
  28. package/dist/commands/tasks/tasks.js +8 -2
  29. package/dist/commands/workflow-cli.js +17 -11
  30. package/dist/core/abort-deadline.js +28 -0
  31. package/dist/core/adapter/adapters/agent-skills-adapter.js +83 -5
  32. package/dist/core/adapter/adapters/akm-adapter.js +13 -10
  33. package/dist/core/adapter/adapters/akm-lint.js +78 -22
  34. package/dist/core/adapter/adapters/akm-task-adapter.js +43 -20
  35. package/dist/core/adapter/adapters/dotenv-adapter.js +21 -0
  36. package/dist/core/adapter/adapters/tool-dir-shared.js +5 -3
  37. package/dist/core/asset/frontmatter.js +10 -1
  38. package/dist/core/common.js +147 -9
  39. package/dist/core/concurrent.js +32 -0
  40. package/dist/core/config/config-io.js +5 -45
  41. package/dist/core/config/schema/engines.js +14 -3
  42. package/dist/core/config/schema/workflow.js +11 -0
  43. package/dist/core/errors.js +25 -0
  44. package/dist/core/events.js +30 -24
  45. package/dist/core/extra-params.js +11 -0
  46. package/dist/core/file-lock.js +7 -1
  47. package/dist/core/fs-txn.js +15 -2
  48. package/dist/core/improve-result.js +5 -0
  49. package/dist/core/json-schema.js +344 -9
  50. package/dist/core/loopback.js +89 -0
  51. package/dist/core/migration-operation.js +17 -2
  52. package/dist/core/path-access.js +107 -0
  53. package/dist/core/paths.js +16 -2
  54. package/dist/core/redaction.js +86 -18
  55. package/dist/core/spawn-env.js +234 -0
  56. package/dist/core/state-db-scope.js +134 -0
  57. package/dist/core/state-db.js +1 -0
  58. package/dist/core/subprocess.js +181 -37
  59. package/dist/core/write-provenance.js +85 -0
  60. package/dist/core/write-source.js +33 -2
  61. package/dist/indexer/db/graph-db.js +17 -6
  62. package/dist/indexer/ensure-index.js +10 -3
  63. package/dist/indexer/index-written-assets.js +17 -2
  64. package/dist/indexer/indexer.js +86 -21
  65. package/dist/indexer/passes/memory-inference.js +4 -0
  66. package/dist/indexer/search/db-search.js +25 -17
  67. package/dist/indexer/walk/walker.js +6 -1
  68. package/dist/integrations/agent/detect.js +13 -1
  69. package/dist/integrations/agent/engine-resolution.js +24 -11
  70. package/dist/integrations/agent/model-aliases.js +1 -1
  71. package/dist/integrations/agent/profiles.js +9 -1
  72. package/dist/integrations/agent/spawn.js +15 -87
  73. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +21 -0
  74. package/dist/integrations/lockfile.js +55 -2
  75. package/dist/llm/client.js +14 -19
  76. package/dist/llm/embedder.js +23 -3
  77. package/dist/llm/embedders/remote.js +27 -2
  78. package/dist/output/html-render.js +40 -1
  79. package/dist/output/text/lint-format.js +17 -4
  80. package/dist/runtime.js +23 -1
  81. package/dist/scripts/akm-migrate-node.js +1714 -836
  82. package/dist/scripts/akm-migrate.js +1682 -804
  83. package/dist/setup/setup.js +22 -7
  84. package/dist/sources/providers/git-install.js +25 -2
  85. package/dist/sources/providers/git-stash.js +19 -0
  86. package/dist/sources/providers/git.js +1 -1
  87. package/dist/sources/snapshot-fetchers/content-extract.js +63 -1
  88. package/dist/sources/snapshot-fetchers/website-ingest.js +126 -20
  89. package/dist/storage/database.js +71 -7
  90. package/dist/storage/engines/sqlite-migrations.js +61 -2
  91. package/dist/storage/managed-db.js +19 -0
  92. package/dist/storage/repositories/index-connection.js +39 -4
  93. package/dist/storage/repositories/index-entries-repository.js +6 -1
  94. package/dist/storage/repositories/index-meta-repository.js +11 -0
  95. package/dist/storage/repositories/index-schema.js +17 -2
  96. package/dist/storage/repositories/index-vec-repository.js +43 -5
  97. package/dist/storage/repositories/workflow-runs-repository.js +66 -13
  98. package/dist/storage/sqlite-pragmas.js +12 -1
  99. package/dist/tasks/log-redaction.js +156 -0
  100. package/dist/tasks/parser.js +82 -5
  101. package/dist/tasks/runner.js +222 -17
  102. package/dist/tasks/scheduler-invocation.js +19 -0
  103. package/dist/tasks/schema.js +86 -1
  104. package/dist/text-import-hook.mjs +1 -1
  105. package/dist/workflows/concurrency-policy.js +95 -1
  106. package/dist/workflows/exec/dispatch-redaction.js +114 -0
  107. package/dist/workflows/exec/exec-unit.js +542 -0
  108. package/dist/workflows/exec/frozen-judge.js +114 -42
  109. package/dist/workflows/exec/native-executor.js +465 -238
  110. package/dist/workflows/exec/param-secrets.js +4 -3
  111. package/dist/workflows/exec/run-workflow.js +424 -219
  112. package/dist/workflows/exec/step-work.js +506 -167
  113. package/dist/workflows/exec/unit-dispatch.js +31 -1
  114. package/dist/workflows/exec/unit-writer.js +53 -13
  115. package/dist/workflows/exec/worktree.js +454 -41
  116. package/dist/workflows/ir/compile.js +26 -2
  117. package/dist/workflows/ir/freeze.js +82 -15
  118. package/dist/workflows/ir/schema.js +105 -20
  119. package/dist/workflows/parser.js +242 -19
  120. package/dist/workflows/program/schema.js +24 -0
  121. package/dist/workflows/renderer.js +32 -4
  122. package/dist/workflows/resource-limits.js +182 -0
  123. package/dist/workflows/runtime/runs.js +146 -6
  124. package/dist/workflows/validate-summary.js +17 -2
  125. package/docs/README.md +74 -32
  126. package/docs/migration/release-notes/0.9.0.md +2 -1
  127. package/docs/migration/v0.7-to-v0.8.md +2 -1
  128. package/docs/migration/v0.8-to-v0.9.md +3 -1
  129. package/docs/reference/README.md +11 -4
  130. package/docs/reference/bundle-types.md +19 -0
  131. package/docs/reference/cli.md +105 -16
  132. package/docs/reference/configuration.md +15 -2
  133. package/docs/reference/data-and-telemetry.md +30 -10
  134. package/docs/reference/supported-formats.md +50 -0
  135. package/docs/reference/workflow-schema.md +1014 -0
  136. package/docs/reference/workflows.md +37 -633
  137. package/package.json +13 -6
  138. package/schemas/akm-config.json +18 -5
  139. package/schemas/akm-task.json +27 -5
  140. package/schemas/akm-workflow.json +92 -13
package/README.md CHANGED
@@ -1,16 +1,17 @@
1
1
  # akm -- Agent Knowledge Manager
2
2
 
3
- > **akm** (Agent Knowledge Manager) -- A knowledge toolkit for AI agents.
3
+ > Give every coding agent the capabilities your team has already built.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/akm-cli)](https://www.npmjs.com/package/akm-cli)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/akm-cli)](https://www.npmjs.com/package/akm-cli)
7
7
  [![license](https://img.shields.io/github/license/itlackey/akm)](https://github.com/itlackey/akm/blob/main/LICENSE)
8
8
 
9
- `akm` is a knowledge toolkit for AI agent capabilities -- scripts, skills, commands,
10
- agents, knowledge, instructions, memories, workflows, env files, secrets, lessons,
11
- tasks, sessions, and facts. It can discover and install bundles from npm, git, local
12
- directories, and configured registries. It works with any AI coding assistant that can run shell commands,
13
- including [Claude Code](https://claude.ai/code),
9
+ **akm** is a portable capability library for AI agents. Build your agent
10
+ library once. Use it from any shell-capable coding agent. It indexes existing
11
+ agent assets in place, loads only what a task needs, packages capabilities
12
+ into shareable bundles, improves the library through reviewable proposals,
13
+ and runs durable workflows -- locally and without tying the library to one
14
+ assistant, including [Claude Code](https://claude.ai/code),
14
15
  [OpenCode](https://opencode.ai), [Cursor](https://cursor.com), and more.
15
16
 
16
17
  ## Install
@@ -35,54 +36,33 @@ Upgrade in place with `akm upgrade`.
35
36
 
36
37
  The npm package always uses Node.js to bootstrap its cross-platform command.
37
38
  If a working [Bun](https://bun.sh) >= 1.0 is also on `PATH`, the launcher
38
- prefers Bun for execution; old, unusable, or absent Bun installations fall back
39
- to Node.js. Node.js remains required for the npm package. The standalone
39
+ prefers Bun for execution; old, unusable, or absent Bun installations fall
40
+ back to Node.js. Node.js remains required for the npm package. The standalone
40
41
  binaries are runtime-free.
41
42
 
42
- > The old `vault` asset type was removed in 0.9.0; use `env` for whole `.env`
43
- > groups and `secret` for standalone sensitive values.
44
-
45
43
  ## Quick Start
46
44
 
47
45
  ```sh
48
- akm setup # Guided setup: configure, initialize, and index
49
- akm task doctor # Verify scheduler and installed runtime
50
- akm bundle add github:owner/repo # Add a bundle from GitHub
51
- akm search "deploy" # Find assets across all sources
52
- akm show scripts/deploy.sh # View details and run command
53
- ```
54
-
55
- Non-interactive setup never activates schedules.
56
-
57
- ## Schedule Tasks Safely
58
-
59
- ```sh
60
- akm setup # Review definitions, schedules, and enabled state
61
- # Confirm scheduler activation only after reviewing the complete task summary.
62
- akm task doctor # Verify backend, runtime, task state, and warnings
46
+ akm setup # Guided setup: configure, initialize, and index
47
+ akm bundle add github:owner/repo # Add a bundle from GitHub
48
+ akm index # Index sources into the library
49
+ akm search "deploy" # Find assets across all sources
50
+ akm show scripts/deploy.sh # View details and run command
63
51
  ```
64
52
 
65
- Setup shows the complete task review — both the general-purpose core
66
- templates and the maintainer-oriented improve cadence — before asking one
67
- explicit question about changing task files and the OS scheduler. Only
68
- confirmation prepares the definitions and syncs the scheduler. Declining, or
69
- running setup non-interactively, leaves both unchanged. A scheduled entry
70
- captures the installed akm runtime used during activation. Ordinary
71
- `akm task sync` preserves that runtime; after moving or replacing the
72
- installation, use `akm task sync --rebind` explicitly to migrate or repair
73
- scheduler entries, then run `akm task doctor` again.
74
-
75
- Rerunning setup preserves existing scheduler bindings. If setup changes the AKM
76
- storage path, or the installed runtime path changes, run
77
- `akm task sync --rebind` explicitly.
78
-
79
53
  ## Why akm?
80
54
 
81
- - **Works with any AI agent** -- No plugins or SDKs required. Any model that can run shell commands can use `akm`.
82
- - **One command to search your knowledge** -- Local assets are searched by default. Add `--from registry` or `--from all` for configured registry results; [skills.sh](https://skills.sh) participates only when enabled.
83
- - **Install bundles from anywhere** -- npm, GitHub, GitLab, local directories.
84
- - **Semantic search** -- Optional local embeddings (via Ollama or HuggingFace) for finding assets by meaning, not just keywords.
85
- - **Private registries** -- Host your own registry for team or enterprise use.
55
+ - **One library for every agent** -- Use the same capability library from Claude Code, OpenCode, Cursor, Aider, Windsurf, or any assistant that can run shell commands.
56
+ - **Load only what the task needs** -- Search or curate a shortlist, then load full content by ref. No giant startup prompt is required.
57
+ - **Package complete capabilities** -- Install and share bundles containing skills, scripts, workflows, agents, instructions, memories, and knowledge -- not just prompt snippets.
58
+ - **Improve through evidence, with review** -- Feedback influences retrieval and produces diffable proposals. Changes remain reviewable and target only writable bundles.
59
+ - **Turn knowledge into repeatable work** -- Run persisted workflows with dispatch, gates, retries, budgets, and resume instead of reconstructing a process from prose every session.
60
+
61
+ akm retrieves every supported capability type. It directly orchestrates
62
+ defined execution surfaces such as workflows, agent dispatch, tasks, and
63
+ guarded subprocess injection -- it does not blindly execute arbitrary indexed
64
+ content merely because that content appears in search results. It
65
+ complements MCP and assistant-native skills rather than replacing them.
86
66
 
87
67
  ## Agent Integration
88
68
 
@@ -96,32 +76,17 @@ knowledge, instructions, memories, workflows, env files, secrets, lessons, tasks
96
76
  sessions, and facts via the `akm` CLI. Use `akm -h` for details.
97
77
  ```
98
78
 
99
- ## Install Bundles from Anywhere
100
-
101
- ```sh
102
- akm bundle add @scope/pkg # npm
103
- akm bundle add github:owner/repo#v1.2.3 # GitHub with tag
104
- akm bundle add git+https://gitlab.com/org/repo # Any git repo
105
- akm bundle add ./path/to/local/dir # Local directory
106
- ```
107
-
108
- Manage bundles with `akm bundle list`, `akm bundle update --all`, and `akm bundle remove`.
109
-
110
- ## Publish Your Own Bundle
111
-
112
- 1. Organize your assets into a directory
113
- 2. Add `"akm"` to `keywords` in `package.json`
114
- 3. Optionally add `akm.include` in `package.json` to control what gets installed
115
- 4. Publish to npm or push to GitHub
79
+ Or generate it directly: `akm help agents >> AGENTS.md`
116
80
 
117
81
  ## Documentation
118
82
 
119
83
  Full docs, CLI reference, and guides are available on [GitHub](https://github.com/itlackey/akm):
120
84
 
121
85
  - [Getting Started](https://github.com/itlackey/akm/blob/main/docs/guides/getting-started.md)
86
+ - [Supported Formats](https://github.com/itlackey/akm/blob/main/docs/reference/supported-formats.md)
122
87
  - [CLI Reference](https://github.com/itlackey/akm/blob/main/docs/reference/cli.md)
123
88
  - [Configuration](https://github.com/itlackey/akm/blob/main/docs/reference/configuration.md)
124
- - [Bundle Maker's Guide](https://github.com/itlackey/akm/blob/main/docs/guides/stash-makers.md)
89
+ - [Bundle Authoring Guide](https://github.com/itlackey/akm/blob/main/docs/guides/author-bundles.md)
125
90
  - [Registry](https://github.com/itlackey/akm/blob/main/docs/reference/registry.md)
126
91
 
127
92
  ## License
package/STABILITY.md CHANGED
@@ -460,8 +460,10 @@ Internal replacement for the one capability nothing else covered.
460
460
  - **0.10 — `--auto-accept` hard error.** It is currently accepted-and-warned;
461
461
  see the Improvement loop entry.
462
462
  - **0.10 — `BundleAdapter.placeNew()` wiring.** The interface declares
463
- `placeNew()` as an optional capability method, and 8 of the built-in
464
- adapters already implement it, but nothing in the write path calls it —
463
+ `placeNew()` as an optional capability method, and 9 of the 11 built-in
464
+ adapters already implement it (all but `okf` and `website-snapshot`;
465
+ `claude` and `opencode` inherit theirs from the shared tool-dir factory),
466
+ but nothing in the write path calls it —
465
467
  writes still resolve through AKM's native flat type→directory table.
466
468
  Placement for every existing bundle is already correct today; this is a
467
469
  deliberately sequenced routing change, not unfinished behavior. See
@@ -45,8 +45,14 @@ export function parsePositiveIntFlag(raw, flagName = "--limit") {
45
45
  const trimmed = raw.trim();
46
46
  if (!trimmed)
47
47
  return undefined;
48
+ // Strict digits, matching parseNonNegativeIntFlag below. parseInt stops at the
49
+ // first non-digit, so "10x" silently became 10, "3.5" became 3, and
50
+ // "5 apples" became 5 — accepted rather than rejected as invalid.
51
+ if (!/^\d+$/.test(trimmed)) {
52
+ throw new UsageError(`Invalid ${flagName} value: "${raw}". Must be a positive integer.`, "INVALID_FLAG_VALUE");
53
+ }
48
54
  const parsed = parseInt(trimmed, 10);
49
- if (Number.isNaN(parsed) || parsed <= 0) {
55
+ if (parsed <= 0) {
50
56
  throw new UsageError(`Invalid ${flagName} value: "${raw}". Must be a positive integer.`, "INVALID_FLAG_VALUE");
51
57
  }
52
58
  return parsed;
@@ -174,7 +174,7 @@ export const lintCommand = defineCommand({
174
174
  },
175
175
  type: {
176
176
  type: "string",
177
- description: "Only lint assets of this type (e.g. workflows, tasks, memories)",
177
+ description: "Only lint assets of this type (e.g. workflows, tasks, memories). akm bundles only — every other adapter validates the whole bundle and warns that the flag had no effect.",
178
178
  default: undefined,
179
179
  },
180
180
  },
@@ -1,6 +1,7 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
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
+ import { WIN32_SPAWN_ENV_FLOOR } from "../../core/spawn-env.js";
4
5
  const CLEAN_ENV_ALLOWLIST = [
5
6
  "HOME",
6
7
  "PATH",
@@ -38,6 +39,19 @@ export function buildChildEnv(parentEnv, options) {
38
39
  if (parentEnv[key] !== undefined)
39
40
  base[key] = parentEnv[key];
40
41
  }
42
+ // The allowlist above is POSIX-shaped. On Windows a child started without
43
+ // SystemRoot/COMSPEC/PATHEXT and friends frequently cannot start at all —
44
+ // which is why every other spawn path in the codebase applies this floor
45
+ // (see spawnEnvNamesFor). `env run --clean` / `secret run --clean` did not,
46
+ // so clean-mode injection was unusable there. The floor is names the OS
47
+ // requires of any child, not user configuration, so it does not weaken what
48
+ // "clean" means about inherited secrets.
49
+ if (process.platform === "win32") {
50
+ for (const key of WIN32_SPAWN_ENV_FLOOR) {
51
+ if (parentEnv[key] !== undefined)
52
+ base[key] = parentEnv[key];
53
+ }
54
+ }
41
55
  }
42
56
  for (const key of options.inherit) {
43
57
  if (parentEnv[key] !== undefined)
@@ -12,6 +12,7 @@ import { FEEDBACK_FAILURE_MODES, loadConfig } from "../core/config/config.js";
12
12
  import { NotFoundError, UsageError } from "../core/errors.js";
13
13
  import { appendEvent } from "../core/events.js";
14
14
  import { resolveMutationTarget } from "../core/mutation-target.js";
15
+ import { isPathAbsent } from "../core/path-access.js";
15
16
  import { getDbPath } from "../core/paths.js";
16
17
  import { withStateDb } from "../core/state-db.js";
17
18
  import { warn } from "../core/warn.js";
@@ -287,7 +288,12 @@ export const feedbackCommand = defineJsonCommand({
287
288
  // background process that holds the writer lock, causing the feedback write
288
289
  // to spin-wait for the full reindex duration. If the DB is absent we give a
289
290
  // clear error below rather than silently triggering a rebuild.
290
- if (!fs.existsSync(getDbPath())) {
291
+ // "Run 'akm index' first" is only true advice for an index that was never
292
+ // built. Told to someone whose index exists but is unreadable it is a lie
293
+ // that sends them to rebuild a file they may not have permission to touch
294
+ // (#791), so that case falls through to `openExistingDatabase` below, which
295
+ // names the path, errno, mode/owner and uid instead.
296
+ if (isPathAbsent(getDbPath())) {
291
297
  throw new UsageError("Index not found. Run 'akm index' first to build the index before recording feedback.", "MISSING_REQUIRED_ARGUMENT", "akm index");
292
298
  }
293
299
  // Feedback writes exactly 2 rows (usage_events + utility_score). SQLite
@@ -20,7 +20,8 @@ function emptyLlmUsageStageAggregate() {
20
20
  reasoningTokens: 0,
21
21
  };
22
22
  }
23
- function emptyLlmUsageAggregate() {
23
+ /** A zeroed aggregate — also the value health reports when it could not read state.db at all (#791). */
24
+ export function emptyLlmUsageAggregate() {
24
25
  return { ...emptyLlmUsageStageAggregate(), byStage: {}, byProcess: {}, byEngine: {} };
25
26
  }
26
27
  /**
@@ -4,9 +4,8 @@
4
4
  /**
5
5
  * The remaining `surfaces` advisory group for `akm health` (meta-review 08).
6
6
  * `stash-git-exposure` (08-F1) shipped first in ./stash-exposure.ts; this
7
- * module adds the other three read-only checks the adjudication approved:
7
+ * module adds the other two read-only checks the adjudication approved:
8
8
  *
9
- * - `secret-file-perms` — env/secret/backup files not 0600, dirs not 0700 (F4)
10
9
  * - `binary-config-skew` — config.json written by a NEWER akm than this binary (F3)
11
10
  * - `egress-endpoints` — the remote-destination list, for eyeball diff (surfaces 3/9)
12
11
  *
@@ -15,77 +14,9 @@
15
14
  * the stash-exposure pattern. `egress-endpoints` is the one informational
16
15
  * (pass-status) entry: it emits whenever any remote endpoint is configured.
17
16
  */
18
- import fs from "node:fs";
19
- import path from "node:path";
20
17
  import { MAX_CONFIG_FILE_BYTES, readTextFileWithLimit } from "../../core/common.js";
21
18
  import { CURRENT_CONFIG_VERSION } from "../../core/config/config-schema.js";
22
19
  import { compareConfigVersion } from "../../core/config/config-version.js";
23
- const GROUP_OTHER_BITS = 0o077;
24
- const OFFENDER_EVIDENCE_CAP = 50;
25
- function modeOctal(mode) {
26
- return (mode & 0o777).toString(8).padStart(3, "0");
27
- }
28
- /**
29
- * `secret-file-perms` (08-F4): flag env/secret/backup files that are not 0600
30
- * and their directories when not 0700. Scans `<stash>/env`, `<stash>/secrets`
31
- * and `<cache>/config-backups`; anything readable by group/other is an
32
- * offender. Silent when every path is tight (or none of the dirs exist).
33
- */
34
- export function collectSecretPermsAdvisory(input, platform = process.platform) {
35
- if (platform === "win32")
36
- return undefined;
37
- const roots = [
38
- path.join(input.stashDir, "env"),
39
- path.join(input.stashDir, "secrets"),
40
- path.join(input.cacheDir, "config-backups"),
41
- ];
42
- const offenders = [];
43
- for (const root of roots) {
44
- let stat;
45
- try {
46
- stat = fs.statSync(root);
47
- }
48
- catch {
49
- continue; // absent → nothing to protect
50
- }
51
- if ((stat.mode & GROUP_OTHER_BITS) !== 0)
52
- offenders.push(`${root}/ (${modeOctal(stat.mode)}, want 700)`);
53
- let entries;
54
- try {
55
- entries = fs.readdirSync(root, { recursive: true });
56
- }
57
- catch {
58
- continue;
59
- }
60
- for (const entry of entries) {
61
- const abs = path.join(root, entry);
62
- let entryStat;
63
- try {
64
- entryStat = fs.statSync(abs);
65
- }
66
- catch {
67
- continue;
68
- }
69
- if ((entryStat.mode & GROUP_OTHER_BITS) === 0)
70
- continue;
71
- offenders.push(entryStat.isDirectory()
72
- ? `${abs}/ (${modeOctal(entryStat.mode)}, want 700)`
73
- : `${abs} (${modeOctal(entryStat.mode)}, want 600)`);
74
- }
75
- }
76
- if (offenders.length === 0)
77
- return undefined;
78
- const preview = offenders.slice(0, 5).join("; ") + (offenders.length > 5 ? `; +${offenders.length - 5} more` : "");
79
- return {
80
- name: "secret-file-perms",
81
- kind: "deterministic",
82
- status: "warn",
83
- confidence: "high",
84
- message: `${offenders.length} env/secret/backup path(s) are readable by group/other: ${preview}. ` +
85
- "Tighten with chmod 600 (files) / chmod 700 (dirs) — these hold tokens, keys, and config snapshots.",
86
- evidence: { offenders: offenders.slice(0, OFFENDER_EVIDENCE_CAP) },
87
- };
88
- }
89
20
  /**
90
21
  * `binary-config-skew` (08-F3): warn when config.json carries a configVersion
91
22
  * NEWER than (or unorderable against) this binary's CURRENT_CONFIG_VERSION —
@@ -163,14 +94,10 @@ export function collectEgressAdvisory(config) {
163
94
  };
164
95
  }
165
96
  /**
166
- * Aggregate the three collectors into the advisories array shape `akmHealth`
167
- * consumes. Order is fixed: perms → skew → egress.
97
+ * Aggregate the two collectors into the advisories array shape `akmHealth`
98
+ * consumes. Order is fixed: skew → egress.
168
99
  */
169
100
  export function collectSurfacesAdvisories(input) {
170
- const results = [
171
- collectSecretPermsAdvisory({ stashDir: input.stashDir, cacheDir: input.cacheDir }, input.platform ?? process.platform),
172
- collectConfigSkewAdvisory(input.configPath),
173
- collectEgressAdvisory(input.config),
174
- ];
101
+ const results = [collectConfigSkewAdvisory(input.configPath), collectEgressAdvisory(input.config)];
175
102
  return results.filter((r) => r !== undefined);
176
103
  }
@@ -9,7 +9,8 @@ import { ConfigError, UsageError } from "../core/errors.js";
9
9
  import { readEvents } from "../core/events.js";
10
10
  import { listTxnJournalsTolerant, TXN_SWEEP_GRACE_MS } from "../core/fs-txn.js";
11
11
  import { openLogsDatabase } from "../core/logs-db.js";
12
- import { getCacheDir, getConfigPath, getDataDir, getStateDbPathInDataDir } from "../core/paths.js";
12
+ import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
13
+ import { getConfigPath, getDataDir, getStateDbPathInDataDir } from "../core/paths.js";
13
14
  import { listExistingTableNames, openStateDatabase } from "../core/state-db.js";
14
15
  import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
15
16
  import { readSemanticStatus } from "../indexer/search/semantic-status.js";
@@ -18,7 +19,7 @@ import { queryTaskHistory } from "../storage/repositories/task-history-repositor
18
19
  import { collectImproveAdvisories } from "./health/advisories.js";
19
20
  import { HEALTH_CHECKS } from "./health/checks.js";
20
21
  import { buildImproveSkipSummary, computeWallTimeStats, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./health/improve-metrics.js";
21
- import { readLlmUsageAggregate } from "./health/llm-usage.js";
22
+ import { emptyLlmUsageAggregate, readLlmUsageAggregate } from "./health/llm-usage.js";
22
23
  import { computeDegradationMetrics, computeDenominatorFixedCoverage, computeEnrichmentMintingRollup, probeStateDbRoundTrip, } from "./health/metrics.js";
23
24
  import { collectStashExposureAdvisory } from "./health/stash-exposure.js";
24
25
  import { collectSurfacesAdvisories } from "./health/surfaces.js";
@@ -228,10 +229,9 @@ function gatherImproveSummaryPhase(db, stateDbPath, since, now) {
228
229
  /**
229
230
  * The three best-effort advisory groups beyond the health-check registry:
230
231
  * improve advisories, the `stash-git-exposure` probe, and the 08 surfaces
231
- * group (secret-file-perms, binary-config-skew, egress-endpoints). Order
232
- * matches emission order in the returned array. A
233
- * probe/filesystem failure in either try/catch must not abort the health
234
- * report — each group degrades to "no advisory" independently.
232
+ * group (binary-config-skew, egress-endpoints). Order matches emission order in
233
+ * the returned array. A probe/filesystem failure in either try/catch must not
234
+ * abort the health report each group degrades to "no advisory" independently.
235
235
  */
236
236
  function gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, options, egressConfigView) {
237
237
  const advisories = [...collectImproveAdvisories(db, stateDbPath, since, improveSummary)];
@@ -254,13 +254,11 @@ function gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, optio
254
254
  catch {
255
255
  // Non-fatal — a git/probe failure must not abort the health report.
256
256
  }
257
- // 08 surfaces: the remaining read-only advisory group (secret-file-perms,
258
- // binary-config-skew, egress-endpoints). Best-effort — a
259
- // filesystem probe failure must not abort the health report.
257
+ // 08 surfaces: the remaining read-only advisory group (binary-config-skew,
258
+ // egress-endpoints). Best-effort — a filesystem probe failure must not abort
259
+ // the health report.
260
260
  try {
261
261
  advisories.push(...collectSurfacesAdvisories({
262
- stashDir: options.stashDir ?? resolveStashDir(),
263
- cacheDir: getCacheDir(),
264
262
  configPath: getConfigPath(),
265
263
  config: egressConfigView,
266
264
  }));
@@ -328,6 +326,53 @@ function resolveWindowComparePhase(options, db, stateDbPath, now, logsDb) {
328
326
  }
329
327
  return { windowResults, deltas };
330
328
  }
329
+ /**
330
+ * The health report for a state.db this process cannot read (#791).
331
+ *
332
+ * `akm health` is what an operator runs when other commands are misbehaving, so
333
+ * it must survive the permission problem long enough to NAME it. Previously it
334
+ * threw `ConfigError` (exit 78) on the state.db open, which meant the one
335
+ * command able to explain a data-directory permission fault died before
336
+ * reaching any of its advisories.
337
+ *
338
+ * Reported as a hard-channel `fail` — the run genuinely could not assess the
339
+ * install — with the path, errno, mode/owner and running uid in the message. It
340
+ * exits non-zero either way; the difference is that the operator is now told
341
+ * WHY instead of being handed a bare "unable to open database file".
342
+ */
343
+ function unreadableStateDbReport(detail, options) {
344
+ return {
345
+ schemaVersion: 3,
346
+ ok: false,
347
+ status: "fail",
348
+ since: parseHealthSince(options.since),
349
+ hardChecks: [
350
+ {
351
+ name: "state-db-readable",
352
+ kind: "deterministic",
353
+ status: "fail",
354
+ confidence: "high",
355
+ message: `state.db exists but is not readable: ${detail}. Every other health check is skipped because ` +
356
+ "none of them can read it. Check the owner and mode of the data directory, or point " +
357
+ "AKM_DATA_DIR / XDG_DATA_HOME at a location this user owns.",
358
+ evidence: { detail },
359
+ },
360
+ ],
361
+ advisories: [],
362
+ metrics: {
363
+ taskFailRate: 0,
364
+ agentFailureRate: 0,
365
+ stuckActiveRuns: 0,
366
+ logBackingRate: 0,
367
+ // `null`, not 0: the round-trip probe did not run, which is not the same
368
+ // as it running instantly.
369
+ probeRoundTripMs: null,
370
+ llmUsage: emptyLlmUsageAggregate(),
371
+ },
372
+ improve: summarizeImproveCompleted([]),
373
+ sessionLogAdvisories: [],
374
+ };
375
+ }
331
376
  export function akmHealth(options = {}) {
332
377
  validateAkmHealthOptions(options);
333
378
  const now = options.now ?? (() => Date.now());
@@ -336,11 +381,20 @@ export function akmHealth(options = {}) {
336
381
  const hardChecks = [];
337
382
  const advisories = [];
338
383
  const getExecutionLogCandidatesFn = options.getExecutionLogCandidatesFn ?? getExecutionLogCandidates;
384
+ // #791: an UNREADABLE state.db is the one failure `akm health` most needs to
385
+ // be able to report, because it is the command an operator runs to find out
386
+ // why everything else is behaving oddly. Dying here with exit 78 meant health
387
+ // could not diagnose that state at all — not even the checks that never touch
388
+ // state.db got to run. Report it as a finding instead.
339
389
  let db;
340
390
  try {
341
391
  db = openStateDatabase(stateDbPath);
342
392
  }
343
393
  catch (error) {
394
+ const { access, code } = classifyPathAccess(stateDbPath);
395
+ if (access === "inaccessible") {
396
+ return unreadableStateDbReport(describeInaccessiblePath(stateDbPath, code), options);
397
+ }
344
398
  throw new ConfigError(`Unable to open state.db: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
345
399
  }
346
400
  // logs.db backs the log-backing metric (#579). Best-effort: when it cannot
@@ -15,6 +15,7 @@ import { timestampForFilename } from "../../../core/common.js";
15
15
  import { appendEvent } from "../../../core/events.js";
16
16
  import { parseEmbeddedJsonResponse } from "../../../core/parse.js";
17
17
  import { withStateDb } from "../../../core/state-db.js";
18
+ import { recordWrittenPath } from "../../../core/write-provenance.js";
18
19
  import { getDefaultLlmConfig } from "../../../integrations/agent/engine-resolution.js";
19
20
  import { callStructured } from "../../../llm/structured-call.js";
20
21
  import { akmSearch } from "../../read/search.js";
@@ -247,7 +248,11 @@ export function writeQualityRejection(stash, inputRef, proposalRef, content, sco
247
248
  const rejectDir = path.join(stash, ".akm", "distill-rejected");
248
249
  fs.mkdirSync(rejectDir, { recursive: true });
249
250
  const ts = timestampForFilename();
250
- fs.writeFileSync(path.join(rejectDir, `${ts}-${proposalRef.replace(/[:/\\]/g, "-")}.md`), `---\nscore: ${score}\nreason: ${reason}\noutcome: ${outcome}\n---\n\n${content}`, "utf8");
251
+ const rejectPath = path.join(rejectDir, `${ts}-${proposalRef.replace(/[:/\\]/g, "-")}.md`);
252
+ fs.writeFileSync(rejectPath, `---\nscore: ${score}\nreason: ${reason}\noutcome: ${outcome}\n---\n\n${content}`, "utf8");
253
+ // #652: the rejection envelope lands under the managed `.akm/` tree, which
254
+ // the pre-provenance sync swept up by pathspec — journal it explicitly.
255
+ recordWrittenPath(rejectPath);
251
256
  appendEvent({
252
257
  eventType: "distill_invoked",
253
258
  ref: inputRef,
@@ -8,6 +8,7 @@ import { conceptIdFromTypeName, parseRefInput, resolveRef } from "../../core/ass
8
8
  import { loadConfig } from "../../core/config/config.js";
9
9
  import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
10
10
  import { readEvents } from "../../core/events.js";
11
+ import { isPathAbsent } from "../../core/path-access.js";
11
12
  import { getDbPath } from "../../core/paths.js";
12
13
  import { deriveInstallations } from "../../indexer/installations.js";
13
14
  import { getWritableStashDirs, resolveSourceEntries } from "../../indexer/search/search-source.js";
@@ -27,11 +28,16 @@ import { improveStateReadRefs } from "./source-identity.js";
27
28
  * yet. `openExistingDatabase` refuses to create a missing `index.db` (see
28
29
  * index-connection.ts) — for eligibility, "no index" simply means nothing is
29
30
  * eligible, exactly like the readOnly arm's `undefined`.
31
+ *
32
+ * `undefined` is reserved for a genuinely ABSENT index. The readOnly arm has
33
+ * refused to conflate that with an unreadable one since #791; this arm used
34
+ * `fs.existsSync`, so the same run reported "nothing eligible to improve" at
35
+ * exit 0 depending only on which branch it took. Both arms now raise.
30
36
  */
31
37
  function openEligibilityDb(readOnly) {
32
38
  if (readOnly)
33
39
  return openReadonlyExistingDatabase();
34
- return fs.existsSync(getDbPath()) ? openExistingDatabase() : undefined;
40
+ return isPathAbsent(getDbPath()) ? undefined : openExistingDatabase();
35
41
  }
36
42
  export function resolveImproveScope(scope) {
37
43
  const trimmed = scope?.trim();
@@ -4,6 +4,7 @@
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { writeFileAtomic } from "../../core/common.js";
7
+ import { recordWrittenPath } from "../../core/write-provenance.js";
7
8
  export function writeEvalCase(stashDir, evalCase) {
8
9
  const evalDir = path.join(stashDir, ".akm", "eval-cases");
9
10
  fs.mkdirSync(evalDir, { recursive: true });
@@ -28,6 +29,7 @@ Use it as a regression test: future improve runs on this ref should not produce
28
29
  output that would be rejected for the same reason.
29
30
  `;
30
31
  writeFileAtomic(filePath, content);
32
+ recordWrittenPath(filePath);
31
33
  return filePath;
32
34
  }
33
35
  export function countEvalCases(stashDir) {