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.
- package/CHANGELOG.md +724 -0
- package/README.md +28 -63
- package/STABILITY.md +4 -2
- package/dist/cli/parse-args.js +7 -1
- package/dist/commands/agent/contribute-cli.js +1 -1
- package/dist/commands/env/child-env.js +14 -0
- package/dist/commands/feedback-cli.js +7 -1
- package/dist/commands/health/llm-usage.js +2 -1
- package/dist/commands/health/surfaces.js +4 -77
- package/dist/commands/health.js +65 -11
- package/dist/commands/improve/distill/quality-gate.js +6 -1
- package/dist/commands/improve/eligibility.js +7 -1
- package/dist/commands/improve/eval-cases.js +2 -0
- package/dist/commands/improve/improve.js +126 -10
- package/dist/commands/improve/locks.js +7 -0
- package/dist/commands/improve/memory/memory-improve.js +9 -0
- package/dist/commands/improve/run-context.js +5 -0
- package/dist/commands/improve/session-asset.js +4 -0
- package/dist/commands/lint/base-linter.js +31 -7
- package/dist/commands/lint/index.js +205 -51
- package/dist/commands/lint/types.js +22 -1
- package/dist/commands/proposal/repository.js +17 -1
- package/dist/commands/sources/add-cli.js +8 -2
- package/dist/commands/sources/info.js +12 -2
- package/dist/commands/sources/installed-stashes.js +6 -1
- package/dist/commands/sources/migration-help.js +12 -3
- package/dist/commands/sources/self-update.js +9 -1
- package/dist/commands/tasks/tasks.js +8 -2
- package/dist/commands/workflow-cli.js +17 -11
- package/dist/core/abort-deadline.js +28 -0
- package/dist/core/adapter/adapters/agent-skills-adapter.js +83 -5
- package/dist/core/adapter/adapters/akm-adapter.js +13 -10
- package/dist/core/adapter/adapters/akm-lint.js +78 -22
- package/dist/core/adapter/adapters/akm-task-adapter.js +43 -20
- package/dist/core/adapter/adapters/dotenv-adapter.js +21 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +5 -3
- package/dist/core/asset/frontmatter.js +10 -1
- package/dist/core/common.js +147 -9
- package/dist/core/concurrent.js +32 -0
- package/dist/core/config/config-io.js +5 -45
- package/dist/core/config/schema/engines.js +14 -3
- package/dist/core/config/schema/workflow.js +11 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/events.js +30 -24
- package/dist/core/extra-params.js +11 -0
- package/dist/core/file-lock.js +7 -1
- package/dist/core/fs-txn.js +15 -2
- package/dist/core/improve-result.js +5 -0
- package/dist/core/json-schema.js +344 -9
- package/dist/core/loopback.js +89 -0
- package/dist/core/migration-operation.js +17 -2
- package/dist/core/path-access.js +107 -0
- package/dist/core/paths.js +16 -2
- package/dist/core/redaction.js +86 -18
- package/dist/core/spawn-env.js +234 -0
- package/dist/core/state-db-scope.js +134 -0
- package/dist/core/state-db.js +1 -0
- package/dist/core/subprocess.js +181 -37
- package/dist/core/write-provenance.js +85 -0
- package/dist/core/write-source.js +33 -2
- package/dist/indexer/db/graph-db.js +17 -6
- package/dist/indexer/ensure-index.js +10 -3
- package/dist/indexer/index-written-assets.js +17 -2
- package/dist/indexer/indexer.js +86 -21
- package/dist/indexer/passes/memory-inference.js +4 -0
- package/dist/indexer/search/db-search.js +25 -17
- package/dist/indexer/walk/walker.js +6 -1
- package/dist/integrations/agent/detect.js +13 -1
- package/dist/integrations/agent/engine-resolution.js +24 -11
- package/dist/integrations/agent/model-aliases.js +1 -1
- package/dist/integrations/agent/profiles.js +9 -1
- package/dist/integrations/agent/spawn.js +15 -87
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +21 -0
- package/dist/integrations/lockfile.js +55 -2
- package/dist/llm/client.js +14 -19
- package/dist/llm/embedder.js +23 -3
- package/dist/llm/embedders/remote.js +27 -2
- package/dist/output/html-render.js +40 -1
- package/dist/output/text/lint-format.js +17 -4
- package/dist/runtime.js +23 -1
- package/dist/scripts/akm-migrate-node.js +1714 -836
- package/dist/scripts/akm-migrate.js +1682 -804
- package/dist/setup/setup.js +22 -7
- package/dist/sources/providers/git-install.js +25 -2
- package/dist/sources/providers/git-stash.js +19 -0
- package/dist/sources/providers/git.js +1 -1
- package/dist/sources/snapshot-fetchers/content-extract.js +63 -1
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -20
- package/dist/storage/database.js +71 -7
- package/dist/storage/engines/sqlite-migrations.js +61 -2
- package/dist/storage/managed-db.js +19 -0
- package/dist/storage/repositories/index-connection.js +39 -4
- package/dist/storage/repositories/index-entries-repository.js +6 -1
- package/dist/storage/repositories/index-meta-repository.js +11 -0
- package/dist/storage/repositories/index-schema.js +17 -2
- package/dist/storage/repositories/index-vec-repository.js +43 -5
- package/dist/storage/repositories/workflow-runs-repository.js +66 -13
- package/dist/storage/sqlite-pragmas.js +12 -1
- package/dist/tasks/log-redaction.js +156 -0
- package/dist/tasks/parser.js +82 -5
- package/dist/tasks/runner.js +222 -17
- package/dist/tasks/scheduler-invocation.js +19 -0
- package/dist/tasks/schema.js +86 -1
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/concurrency-policy.js +95 -1
- package/dist/workflows/exec/dispatch-redaction.js +114 -0
- package/dist/workflows/exec/exec-unit.js +542 -0
- package/dist/workflows/exec/frozen-judge.js +114 -42
- package/dist/workflows/exec/native-executor.js +465 -238
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +424 -219
- package/dist/workflows/exec/step-work.js +506 -167
- package/dist/workflows/exec/unit-dispatch.js +31 -1
- package/dist/workflows/exec/unit-writer.js +53 -13
- package/dist/workflows/exec/worktree.js +454 -41
- package/dist/workflows/ir/compile.js +26 -2
- package/dist/workflows/ir/freeze.js +82 -15
- package/dist/workflows/ir/schema.js +105 -20
- package/dist/workflows/parser.js +242 -19
- package/dist/workflows/program/schema.js +24 -0
- package/dist/workflows/renderer.js +32 -4
- package/dist/workflows/resource-limits.js +182 -0
- package/dist/workflows/runtime/runs.js +146 -6
- package/dist/workflows/validate-summary.js +17 -2
- package/docs/README.md +74 -32
- package/docs/migration/release-notes/0.9.0.md +2 -1
- package/docs/migration/v0.7-to-v0.8.md +2 -1
- package/docs/migration/v0.8-to-v0.9.md +3 -1
- package/docs/reference/README.md +11 -4
- package/docs/reference/bundle-types.md +19 -0
- package/docs/reference/cli.md +105 -16
- package/docs/reference/configuration.md +15 -2
- package/docs/reference/data-and-telemetry.md +30 -10
- package/docs/reference/supported-formats.md +50 -0
- package/docs/reference/workflow-schema.md +1014 -0
- package/docs/reference/workflows.md +37 -633
- package/package.json +13 -6
- package/schemas/akm-config.json +18 -5
- package/schemas/akm-task.json +27 -5
- package/schemas/akm-workflow.json +92 -13
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# akm -- Agent Knowledge Manager
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Give every coding agent the capabilities your team has already built.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/akm-cli)
|
|
6
6
|
[](https://www.npmjs.com/package/akm-cli)
|
|
7
7
|
[](https://github.com/itlackey/akm/blob/main/LICENSE)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
|
49
|
-
akm
|
|
50
|
-
akm
|
|
51
|
-
akm search "deploy"
|
|
52
|
-
akm show scripts/deploy.sh
|
|
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
|
-
- **
|
|
82
|
-
- **
|
|
83
|
-
- **Install bundles
|
|
84
|
-
- **
|
|
85
|
-
- **
|
|
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
|
-
|
|
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
|
|
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
|
|
464
|
-
adapters already implement 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
|
package/dist/cli/parse-args.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
167
|
-
* consumes. Order is fixed:
|
|
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
|
}
|
package/dist/commands/health.js
CHANGED
|
@@ -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 {
|
|
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 (
|
|
232
|
-
*
|
|
233
|
-
*
|
|
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 (
|
|
258
|
-
//
|
|
259
|
-
//
|
|
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
|
-
|
|
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
|
|
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) {
|