akm-cli 0.9.0-beta.50 → 0.9.0-beta.52
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 +2 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/wiki/ingest-workflow-template.md +34 -12
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli.js +12 -6
- package/dist/commands/config-cli.js +18 -2
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +17 -2
- package/dist/commands/env/secret-cli.js +24 -2
- package/dist/commands/health/checks.js +1 -1
- package/dist/commands/improve/improve-auto-accept.js +30 -2
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-result-file.js +9 -2
- package/dist/commands/improve/preparation.js +10 -2
- package/dist/commands/improve/recombine.js +52 -15
- package/dist/commands/lint/env-key-rules.js +4 -0
- package/dist/commands/read/knowledge.js +5 -2
- package/dist/commands/read/search-cli.js +2 -4
- package/dist/commands/read/search.js +9 -6
- package/dist/commands/read/show.js +19 -5
- package/dist/commands/sources/init.js +13 -8
- package/dist/commands/sources/installed-stashes.js +6 -2
- package/dist/commands/sources/schema-repair.js +33 -47
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/tasks/tasks.js +38 -10
- package/dist/core/asset/asset-registry.js +1 -1
- package/dist/core/asset/asset-spec.js +4 -2
- package/dist/core/config/config-migration.js +12 -11
- package/dist/indexer/passes/memory-inference.js +3 -2
- package/dist/indexer/search/db-search.js +6 -4
- package/dist/indexer/search/search-source.js +15 -2
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -135
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/renderers.js +1 -1
- package/dist/scripts/migrate-storage.js +463 -347
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +99 -99
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/git-install.js +10 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/website.js +8 -3
- package/dist/sources/website-ingest.js +136 -20
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +15 -11
- package/docs/data-and-telemetry.md +2 -2
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
[](https://github.com/itlackey/akm/blob/main/LICENSE)
|
|
8
8
|
|
|
9
9
|
`akm` is a package manager for AI agent capabilities -- scripts, skills, commands,
|
|
10
|
-
agents, knowledge, memories, workflows, wikis,
|
|
11
|
-
tasks. It works with any AI coding assistant that can run shell commands,
|
|
10
|
+
agents, knowledge, memories, workflows, wikis, env files, secrets, lessons, and
|
|
11
|
+
scheduled tasks. It works with any AI coding assistant that can run shell commands,
|
|
12
12
|
including [Claude Code](https://claude.ai/code),
|
|
13
13
|
[OpenCode](https://opencode.ai), [Cursor](https://cursor.com), and more.
|
|
14
14
|
|
|
@@ -30,9 +30,17 @@ irm https://github.com/itlackey/akm/releases/latest/download/install.ps1 | iex
|
|
|
30
30
|
bun install -g akm-cli
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
**Option 3 — Node.js (requires Node.js >= 20.12):**
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm install -g akm-cli
|
|
37
|
+
```
|
|
38
|
+
|
|
33
39
|
Upgrade in place with `akm upgrade`.
|
|
34
40
|
|
|
35
|
-
> **AKM 0.
|
|
41
|
+
> **AKM 0.9.0 supports three install paths:** prebuilt binary, Bun, or Node.js >= 20.12.
|
|
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.
|
|
36
44
|
|
|
37
45
|
## Quick Start
|
|
38
46
|
|
|
@@ -59,7 +67,7 @@ Add this to your `AGENTS.md`, `CLAUDE.md`, or system prompt:
|
|
|
59
67
|
## Resources & Capabilities
|
|
60
68
|
|
|
61
69
|
You have access to a searchable library of scripts, skills, commands, agents,
|
|
62
|
-
knowledge, memories, workflows, wikis,
|
|
70
|
+
knowledge, memories, workflows, wikis, env files, secrets, lessons, and scheduled tasks
|
|
63
71
|
via the `akm` CLI. Use `akm -h` for details.
|
|
64
72
|
```
|
|
65
73
|
|
package/dist/akm
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
|
|
6
|
+
case "$0" in
|
|
7
|
+
*/*) SCRIPT_PATH=$0 ;;
|
|
8
|
+
*)
|
|
9
|
+
SCRIPT_PATH=$(command -v -- "$0" 2>/dev/null || true)
|
|
10
|
+
if [ -z "$SCRIPT_PATH" ]; then
|
|
11
|
+
echo "akm launcher could not resolve its own path." >&2
|
|
12
|
+
exit 127
|
|
13
|
+
fi
|
|
14
|
+
;;
|
|
15
|
+
esac
|
|
16
|
+
|
|
17
|
+
if command -v readlink >/dev/null 2>&1; then
|
|
18
|
+
while [ -L "$SCRIPT_PATH" ]; do
|
|
19
|
+
LINK_TARGET=$(readlink "$SCRIPT_PATH")
|
|
20
|
+
case "$LINK_TARGET" in
|
|
21
|
+
/*) SCRIPT_PATH=$LINK_TARGET ;;
|
|
22
|
+
*) SCRIPT_PATH=${SCRIPT_PATH%/*}/$LINK_TARGET ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
SCRIPT_DIR=$(CDPATH= cd -- "${SCRIPT_PATH%/*}" && pwd)
|
|
28
|
+
|
|
29
|
+
if command -v bun >/dev/null 2>&1; then
|
|
30
|
+
exec bun "$SCRIPT_DIR/cli.js" "$@"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if command -v node >/dev/null 2>&1; then
|
|
34
|
+
exec node "$SCRIPT_DIR/cli-node.mjs" "$@"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
echo "akm requires Bun or Node.js >= 20.12.0 on PATH." >&2
|
|
38
|
+
exit 127
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
|
|
6
|
+
case "$0" in
|
|
7
|
+
*/*) SCRIPT_PATH=$0 ;;
|
|
8
|
+
*)
|
|
9
|
+
SCRIPT_PATH=$(command -v -- "$0" 2>/dev/null || true)
|
|
10
|
+
if [ -z "$SCRIPT_PATH" ]; then
|
|
11
|
+
echo "akm-migrate-storage launcher could not resolve its own path." >&2
|
|
12
|
+
exit 127
|
|
13
|
+
fi
|
|
14
|
+
;;
|
|
15
|
+
esac
|
|
16
|
+
|
|
17
|
+
if command -v readlink >/dev/null 2>&1; then
|
|
18
|
+
while [ -L "$SCRIPT_PATH" ]; do
|
|
19
|
+
LINK_TARGET=$(readlink "$SCRIPT_PATH")
|
|
20
|
+
case "$LINK_TARGET" in
|
|
21
|
+
/*) SCRIPT_PATH=$LINK_TARGET ;;
|
|
22
|
+
*) SCRIPT_PATH=${SCRIPT_PATH%/*}/$LINK_TARGET ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
SCRIPT_DIR=$(CDPATH= cd -- "${SCRIPT_PATH%/*}" && pwd)
|
|
28
|
+
|
|
29
|
+
if command -v bun >/dev/null 2>&1; then
|
|
30
|
+
exec bun "$SCRIPT_DIR/scripts/migrate-storage.js" "$@"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if command -v node >/dev/null 2>&1; then
|
|
34
|
+
exec node "$SCRIPT_DIR/migrate-storage-node.mjs" "$@"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
echo "akm-migrate-storage requires Bun or Node.js >= 20.12.0 on PATH." >&2
|
|
38
|
+
exit 127
|
|
@@ -20,16 +20,36 @@ empty and the caller explicitly asked for interactive ingest.
|
|
|
20
20
|
```
|
|
21
21
|
Focus on `uncited-raw` findings: those raw files exist under `raw/` but are
|
|
22
22
|
not yet cited by any authored page. Treat each `uncited-raw` finding as a
|
|
23
|
-
pending ingest item
|
|
24
|
-
|
|
23
|
+
pending ingest item, and sort the queue **oldest raw file first** (by
|
|
24
|
+
filename/mtime). Processing oldest-first keeps backlog age bounded even
|
|
25
|
+
when the queue is larger than one run can finish. If there are no
|
|
26
|
+
`uncited-raw` findings, exit cleanly after a final `akm index` +
|
|
27
|
+
`akm wiki lint {{WIKI_NAME}}` verification.
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
Do not read or classify the whole backlog upfront. Work the queue as a
|
|
30
|
+
bounded loop, one raw file at a time: fully finish a raw (read → decide →
|
|
31
|
+
edit → log entry, steps 3-7 below) before looking at the next one. If you
|
|
32
|
+
run low on time partway through a large backlog, stop after finishing your
|
|
33
|
+
current raw — do not start a new one you can't complete. This is expected
|
|
34
|
+
and fine: whatever you already merged is committed to the pages and
|
|
35
|
+
`log.md`, so the next scheduled run picks up where you left off instead of
|
|
36
|
+
redoing this run's work.
|
|
37
|
+
|
|
38
|
+
3. **For the current raw file, read the source and find related pages.**
|
|
39
|
+
Open the raw file directly from `{{WIKI_DIR}}/raw/`.
|
|
40
|
+
|
|
41
|
+
If the file does not read as text (binary content, garbled bytes, e.g. a
|
|
42
|
+
raw PDF byte-dump that was never text-extracted), do not attempt deep
|
|
43
|
+
forensic recovery (no web searches, no `strings`-style dumps). Append a
|
|
44
|
+
one-line `log.md` entry noting it as skipped/unprocessable with a short
|
|
45
|
+
reason, then move on to the next raw in the queue.
|
|
46
|
+
|
|
47
|
+
Otherwise, search for related pages:
|
|
28
48
|
```sh
|
|
29
49
|
akm wiki search {{WIKI_NAME}} "<key terms from the raw source>"
|
|
30
50
|
```
|
|
31
|
-
Read the top hits with `akm show wiki:{{WIKI_NAME}}/<page>`. Use
|
|
32
|
-
`akm show wiki:{{WIKI_NAME}}/<page> toc` for large pages.
|
|
51
|
+
Read the top hits with `akm show wiki:{{WIKI_NAME}}/pages/<page>`. Use
|
|
52
|
+
`akm show wiki:{{WIKI_NAME}}/pages/<page> toc` for large pages.
|
|
33
53
|
|
|
34
54
|
4. **Decide for each candidate.** For each related page:
|
|
35
55
|
- **Append**: add a section or paragraph under the relevant heading.
|
|
@@ -38,16 +58,18 @@ empty and the caller explicitly asked for interactive ingest.
|
|
|
38
58
|
Follow the schema's contradiction policy.
|
|
39
59
|
- **Skip**: source doesn't add to this page — move on.
|
|
40
60
|
|
|
41
|
-
5. **Create new pages for concepts/entities the source introduces
|
|
42
|
-
|
|
43
|
-
`
|
|
44
|
-
|
|
61
|
+
5. **Create new pages for concepts/entities the source introduces**, under
|
|
62
|
+
`{{WIKI_DIR}}/pages/` (never at the wiki root — only `schema.md`,
|
|
63
|
+
`index.md`, and `log.md` belong there). Each new page must have
|
|
64
|
+
frontmatter with `description`, `pageKind`, `xrefs`, and `sources`.
|
|
65
|
+
Cross-reference with related pages both directions.
|
|
45
66
|
|
|
46
67
|
6. **Update xrefs both ways.** If page A now xrefs page B, page B must xref
|
|
47
68
|
page A. `akm wiki lint {{WIKI_NAME}}` will flag violations.
|
|
48
69
|
|
|
49
|
-
7. **Append to `log.md`.** One entry per
|
|
50
|
-
one-line summary, refs to
|
|
70
|
+
7. **Append to `log.md`.** One entry per processed raw source (ingested or
|
|
71
|
+
skipped-as-unprocessable): date, raw slug, one-line summary, refs to
|
|
72
|
+
created/edited pages. Newest at the top.
|
|
51
73
|
|
|
52
74
|
8. **Regenerate the index + verify.**
|
|
53
75
|
```sh
|
|
@@ -8,7 +8,7 @@ wikiRole: schema
|
|
|
8
8
|
This wiki follows the three-layer pattern:
|
|
9
9
|
|
|
10
10
|
- `raw/` — immutable ingested sources (never edit)
|
|
11
|
-
-
|
|
11
|
+
- `pages/<page>.md` and `pages/<topic>/<page>.md` — agent-authored pages
|
|
12
12
|
- `schema.md` (this file), `index.md`, `log.md` — wiki-level metadata
|
|
13
13
|
|
|
14
14
|
## Page frontmatter
|
|
@@ -20,7 +20,7 @@ Every page should carry frontmatter so akm can index and link it:
|
|
|
20
20
|
description: one-sentence summary used in search and lint
|
|
21
21
|
pageKind: entity | concept | question | note | <your-custom-kind>
|
|
22
22
|
xrefs:
|
|
23
|
-
- wiki:{{WIKI_NAME}}/other-page
|
|
23
|
+
- wiki:{{WIKI_NAME}}/pages/other-page
|
|
24
24
|
sources:
|
|
25
25
|
- raw/<slug>.md
|
|
26
26
|
---
|
|
@@ -36,14 +36,14 @@ will surface in `index.md` as new sections after the next `akm index` run.
|
|
|
36
36
|
1. Copy the new source into `raw/` with `akm wiki stash {{WIKI_NAME}} <path>`.
|
|
37
37
|
2. Find related pages: `akm wiki search {{WIKI_NAME}} "<terms>"`.
|
|
38
38
|
3. For each related page: append a section, note a contradiction, or create a
|
|
39
|
-
new page
|
|
39
|
+
new page under `pages/`. Update xrefs on both sides.
|
|
40
40
|
4. Cite the raw source in each touched page's `sources:` frontmatter.
|
|
41
41
|
5. Append one entry to `log.md` describing what was assimilated.
|
|
42
42
|
|
|
43
43
|
### Query
|
|
44
44
|
|
|
45
45
|
1. `akm wiki search {{WIKI_NAME}} "<question>"` — find candidate pages.
|
|
46
|
-
2. `akm show wiki:{{WIKI_NAME}}/<page>` — read the top hits.
|
|
46
|
+
2. `akm show wiki:{{WIKI_NAME}}/pages/<page>` — read the top hits.
|
|
47
47
|
3. Compose the answer from the wiki; cite raw sources only when the wiki
|
|
48
48
|
points at them.
|
|
49
49
|
|
package/dist/cli/parse-args.js
CHANGED
|
@@ -8,7 +8,52 @@
|
|
|
8
8
|
* main CLI file focused on command definitions and routing.
|
|
9
9
|
*/
|
|
10
10
|
import { UsageError } from "../core/errors.js";
|
|
11
|
-
|
|
11
|
+
function cittyComparableName(name) {
|
|
12
|
+
return name.replace(/[-_]+([a-zA-Z0-9])/g, (_match, char) => char.toUpperCase());
|
|
13
|
+
}
|
|
14
|
+
function toAliasArray(alias) {
|
|
15
|
+
if (Array.isArray(alias))
|
|
16
|
+
return alias;
|
|
17
|
+
return typeof alias === "string" ? [alias] : [];
|
|
18
|
+
}
|
|
19
|
+
function isCittyValueFlag(flag, argsDef) {
|
|
20
|
+
const name = flag.replace(/^-{1,2}/, "");
|
|
21
|
+
const normalized = cittyComparableName(name);
|
|
22
|
+
for (const [key, def] of Object.entries(argsDef)) {
|
|
23
|
+
if (def.type !== "string" && def.type !== "enum")
|
|
24
|
+
continue;
|
|
25
|
+
if (normalized === cittyComparableName(key))
|
|
26
|
+
return true;
|
|
27
|
+
if (toAliasArray(def.alias).includes(name))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Match citty's top-level subcommand scan (`findSubCommandIndex`).
|
|
34
|
+
*
|
|
35
|
+
* Citty does not assume `rawArgs[0]` is the command: global string flags may
|
|
36
|
+
* appear first and consume the following token. The CLI startup guard uses this
|
|
37
|
+
* to classify the requested command before any command handler can run.
|
|
38
|
+
*/
|
|
39
|
+
export function findCittyTopLevelCommandIndex(rawArgs, argsDef) {
|
|
40
|
+
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
41
|
+
const arg = rawArgs[i];
|
|
42
|
+
if (arg === "--")
|
|
43
|
+
return -1;
|
|
44
|
+
if (arg.startsWith("-")) {
|
|
45
|
+
if (!arg.includes("=") && isCittyValueFlag(arg, argsDef))
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
return i;
|
|
50
|
+
}
|
|
51
|
+
return -1;
|
|
52
|
+
}
|
|
53
|
+
export function findCittyTopLevelCommand(rawArgs, argsDef) {
|
|
54
|
+
const index = findCittyTopLevelCommandIndex(rawArgs, argsDef);
|
|
55
|
+
return index >= 0 ? rawArgs[index] : undefined;
|
|
56
|
+
}
|
|
12
57
|
/**
|
|
13
58
|
* Return true when `args._[0]` is a member of `validSet`.
|
|
14
59
|
*
|
package/dist/cli.js
CHANGED
|
@@ -8,18 +8,21 @@
|
|
|
8
8
|
// `dist/cli-node.mjs` wrapper, which registers the text-import loader hook
|
|
9
9
|
// before this module graph loads; running `node dist/cli.js` directly still
|
|
10
10
|
// works for code paths that touch no embedded text asset, but the wrapper is
|
|
11
|
-
// the supported entry. The hard floor is Node 20: `@clack/core` (prompts) imports
|
|
11
|
+
// the supported entry. The hard floor is Node 20.12: `@clack/core` (prompts) imports
|
|
12
12
|
// `node:util`'s `styleText` (added in Node 20.12) — Node 18 (EOL) throws at import.
|
|
13
13
|
{
|
|
14
14
|
const isBun = typeof globalThis.Bun !== "undefined";
|
|
15
15
|
if (!isBun) {
|
|
16
|
-
const major =
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const [major = 0, minor = 0, patch = 0] = (process.versions.node ?? "0")
|
|
17
|
+
.split(".")
|
|
18
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
19
|
+
const nodeOk = major > 20 || (major === 20 && (minor > 12 || (minor === 12 && patch >= 0)));
|
|
20
|
+
if (!nodeOk) {
|
|
21
|
+
console.error("\n ERROR: akm-cli requires the Bun runtime (https://bun.sh) or Node.js >= 20.12.\n" +
|
|
19
22
|
` Detected Node.js ${process.versions.node ?? "unknown"}.\n` +
|
|
20
23
|
" Install options:\n" +
|
|
21
24
|
" 1. Bun: curl -fsSL https://bun.sh/install | bash && bun install -g akm-cli\n" +
|
|
22
|
-
" 2. Node: upgrade to Node.js 20 or newer (https://nodejs.org)\n" +
|
|
25
|
+
" 2. Node: upgrade to Node.js 20.12 or newer (https://nodejs.org)\n" +
|
|
23
26
|
" 3. Binary: curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\n");
|
|
24
27
|
process.exit(1);
|
|
25
28
|
}
|
|
@@ -57,6 +60,7 @@ process.on("uncaughtException", (err) => {
|
|
|
57
60
|
import fs from "node:fs";
|
|
58
61
|
import path from "node:path";
|
|
59
62
|
import { defineCommand, runMain } from "citty";
|
|
63
|
+
import { findCittyTopLevelCommand } from "./cli/parse-args.js";
|
|
60
64
|
import { EXIT_CODES, emitJsonError, output, parseAllFlagValues, runWithJsonErrors } from "./cli/shared.js";
|
|
61
65
|
import { agentCommand, lintCommand, proposeCommand } from "./commands/agent/contribute-cli.js";
|
|
62
66
|
import { generateBashCompletions, installBashCompletions } from "./commands/completions.js";
|
|
@@ -520,6 +524,7 @@ export const main = defineCommand({
|
|
|
520
524
|
tasks: tasksCommand,
|
|
521
525
|
},
|
|
522
526
|
});
|
|
527
|
+
const MAIN_TOP_LEVEL_ARGS = main.args;
|
|
523
528
|
// ── Exit codes ──────────────────────────────────────────────────────────────
|
|
524
529
|
// Canonical table lives in `src/cli/shared.ts` (EXIT_CODES). These aliases keep
|
|
525
530
|
// the local call sites terse. EXIT_HEALTH_WARN (4) is the `akm health` "warn"
|
|
@@ -567,7 +572,8 @@ if (import.meta.main || process.env.AKM_NODE_ENTRY === "1") {
|
|
|
567
572
|
// output-shaping time after the side effect has already happened. The
|
|
568
573
|
// shape-registry gate in shapeForCommand() remains as defense-in-depth (and
|
|
569
574
|
// covers the in-process test harness, which skips this startup block).
|
|
570
|
-
|
|
575
|
+
const topLevelCommand = findCittyTopLevelCommand(process.argv.slice(2), MAIN_TOP_LEVEL_ARGS);
|
|
576
|
+
if (getOutputMode().shape === "summary" && topLevelCommand !== "show") {
|
|
571
577
|
emitJsonError(new UsageError("'--shape summary' is only valid on 'akm show'.", "INVALID_SHAPE_VALUE"));
|
|
572
578
|
}
|
|
573
579
|
// One-time cleanup of stale 0.7.x index file at the old cache location.
|
|
@@ -64,7 +64,10 @@ function rewriteKey(config, key) {
|
|
|
64
64
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
65
65
|
export function getConfigValue(config, key) {
|
|
66
66
|
const k = rewriteKey(config, key);
|
|
67
|
-
|
|
67
|
+
const value = configGet(config, k);
|
|
68
|
+
if (k.split(".").at(-1) === "apiKey")
|
|
69
|
+
return null;
|
|
70
|
+
return omitApiKeysForOutput(value);
|
|
68
71
|
}
|
|
69
72
|
export function setConfigValue(config, key, rawValue) {
|
|
70
73
|
// #454: reject the legacy aliases up front so the error message names the
|
|
@@ -154,7 +157,20 @@ export function listConfig(config) {
|
|
|
154
157
|
result.archiveRetentionDays = config.archiveRetentionDays;
|
|
155
158
|
if (config.configVersion !== undefined)
|
|
156
159
|
result.configVersion = config.configVersion;
|
|
157
|
-
return result;
|
|
160
|
+
return omitApiKeysForOutput(result);
|
|
161
|
+
}
|
|
162
|
+
function omitApiKeysForOutput(value) {
|
|
163
|
+
if (Array.isArray(value))
|
|
164
|
+
return value.map(omitApiKeysForOutput);
|
|
165
|
+
if (!value || typeof value !== "object")
|
|
166
|
+
return value;
|
|
167
|
+
const out = {};
|
|
168
|
+
for (const [key, child] of Object.entries(value)) {
|
|
169
|
+
if (key === "apiKey")
|
|
170
|
+
continue;
|
|
171
|
+
out[key] = omitApiKeysForOutput(child);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
158
174
|
}
|
|
159
175
|
export { unknownKeyHint };
|
|
160
176
|
// ── `akm config` command surface ────────────────────────────────────────────
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
const CLEAN_ENV_ALLOWLIST = [
|
|
5
|
+
"HOME",
|
|
6
|
+
"PATH",
|
|
7
|
+
"PWD",
|
|
8
|
+
"SHELL",
|
|
9
|
+
"TERM",
|
|
10
|
+
"TMPDIR",
|
|
11
|
+
"TEMP",
|
|
12
|
+
"TMP",
|
|
13
|
+
"USER",
|
|
14
|
+
"LOGNAME",
|
|
15
|
+
"LANG",
|
|
16
|
+
"LANGUAGE",
|
|
17
|
+
"LC_ALL",
|
|
18
|
+
"LC_CTYPE",
|
|
19
|
+
"LC_COLLATE",
|
|
20
|
+
"LC_MESSAGES",
|
|
21
|
+
"LC_MONETARY",
|
|
22
|
+
"LC_NUMERIC",
|
|
23
|
+
"LC_TIME",
|
|
24
|
+
"LC_PAPER",
|
|
25
|
+
"LC_NAME",
|
|
26
|
+
"LC_ADDRESS",
|
|
27
|
+
"LC_TELEPHONE",
|
|
28
|
+
"LC_MEASUREMENT",
|
|
29
|
+
"LC_IDENTIFICATION",
|
|
30
|
+
"TZ",
|
|
31
|
+
"NO_COLOR",
|
|
32
|
+
"COLORTERM",
|
|
33
|
+
];
|
|
34
|
+
export function buildChildEnv(parentEnv, options) {
|
|
35
|
+
const base = options.clean ? {} : { ...parentEnv };
|
|
36
|
+
if (options.clean) {
|
|
37
|
+
for (const key of CLEAN_ENV_ALLOWLIST) {
|
|
38
|
+
if (parentEnv[key] !== undefined)
|
|
39
|
+
base[key] = parentEnv[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const key of options.inherit) {
|
|
43
|
+
if (parentEnv[key] !== undefined)
|
|
44
|
+
base[key] = parentEnv[key];
|
|
45
|
+
}
|
|
46
|
+
return base;
|
|
47
|
+
}
|
|
@@ -32,6 +32,7 @@ import { isQuiet } from "../../core/warn.js";
|
|
|
32
32
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
33
33
|
import { getHyphenatedArg, parseFlagValue } from "../../output/context.js";
|
|
34
34
|
import { readStdin } from "../../runtime.js";
|
|
35
|
+
import { buildChildEnv } from "./child-env.js";
|
|
35
36
|
/**
|
|
36
37
|
* Walk each stash's env files and return one entry per `.env` file, using the
|
|
37
38
|
* env asset spec's canonical-name logic (e.g. `env/team/prod.env` →
|
|
@@ -297,7 +298,10 @@ async function runEnvInjected(target, opts) {
|
|
|
297
298
|
}
|
|
298
299
|
process.stderr.write(`warning: ${detail} Injecting anyway (first-party stash).\n`);
|
|
299
300
|
}
|
|
300
|
-
const mergedEnv =
|
|
301
|
+
const mergedEnv = buildChildEnv(process.env, {
|
|
302
|
+
clean: opts.clean === true,
|
|
303
|
+
inherit: opts.inherit ?? [],
|
|
304
|
+
});
|
|
301
305
|
for (const [envKey, envValue] of Object.entries(envValues)) {
|
|
302
306
|
mergedEnv[envKey] = envValue;
|
|
303
307
|
}
|
|
@@ -341,7 +345,7 @@ const envRunCommand = defineCommand({
|
|
|
341
345
|
name: "run",
|
|
342
346
|
description:
|
|
343
347
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal `${secret:NAME}` token syntax documented for users, not interpolation
|
|
344
|
-
"Run a command with the env file injected into its environment: `akm env run <ref> -- <command>`. Use `-- $SHELL` for an interactive session. Restrict which variables are injected with --only / --except. Values may embed `${secret:NAME}` tokens, replaced at run time with the sibling `secret:NAME` value from the same stash.",
|
|
348
|
+
"Run a command with the env file injected into its environment: `akm env run <ref> -- <command>`. Use `-- $SHELL` for an interactive session. Restrict which variables are injected with --only / --except. Values may embed `${secret:NAME}` tokens, replaced at run time with the sibling `secret:NAME` value from the same stash. Pass --clean to start the child with a minimal inherited environment instead of the full parent environment.",
|
|
345
349
|
},
|
|
346
350
|
args: {
|
|
347
351
|
target: { type: "positional", description: "Env ref", required: true },
|
|
@@ -350,11 +354,22 @@ const envRunCommand = defineCommand({
|
|
|
350
354
|
description: "Inject ONLY these keys (comma-separated). Mutually exclusive with --except.",
|
|
351
355
|
},
|
|
352
356
|
except: { type: "string", description: "Inject all keys EXCEPT these (comma-separated)." },
|
|
357
|
+
clean: {
|
|
358
|
+
type: "boolean",
|
|
359
|
+
description: "Start the child with a minimal inherited environment (PATH/HOME/locale/terminal basics) instead of the full parent environment.",
|
|
360
|
+
default: false,
|
|
361
|
+
},
|
|
362
|
+
inherit: {
|
|
363
|
+
type: "string",
|
|
364
|
+
description: "When used with --clean, also inherit these parent env vars (comma-separated). Ignored without --clean.",
|
|
365
|
+
},
|
|
353
366
|
},
|
|
354
367
|
run({ args }) {
|
|
355
368
|
return runWithJsonErrors(() => runEnvInjected(args.target, {
|
|
356
369
|
only: parseKeyListFlag(getHyphenatedArg(args, "only")),
|
|
357
370
|
except: parseKeyListFlag(getHyphenatedArg(args, "except")),
|
|
371
|
+
clean: getHyphenatedArg(args, "clean") === true,
|
|
372
|
+
inherit: parseKeyListFlag(getHyphenatedArg(args, "inherit")) ?? [],
|
|
358
373
|
}));
|
|
359
374
|
},
|
|
360
375
|
});
|
|
@@ -29,6 +29,16 @@ import { appendEvent } from "../../core/events.js";
|
|
|
29
29
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
30
30
|
import { getHyphenatedArg } from "../../output/context.js";
|
|
31
31
|
import { readStdin } from "../../runtime.js";
|
|
32
|
+
import { buildChildEnv } from "./child-env.js";
|
|
33
|
+
function parseKeyListFlag(raw) {
|
|
34
|
+
if (raw === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const keys = raw
|
|
37
|
+
.split(/[,\s]+/)
|
|
38
|
+
.map((k) => k.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
return keys.length > 0 ? keys : undefined;
|
|
41
|
+
}
|
|
32
42
|
/** Walk `secrets/` across all stashes, returning one entry per secret file. */
|
|
33
43
|
function listSecretsRecursive() {
|
|
34
44
|
const result = [];
|
|
@@ -152,11 +162,20 @@ const secretPathCommand = defineCommand({
|
|
|
152
162
|
const secretRunCommand = defineCommand({
|
|
153
163
|
meta: {
|
|
154
164
|
name: "run",
|
|
155
|
-
description: "Run a command with a secret's value injected into an env var: `akm secret run <ref> <VAR> -- <command>`. The value is set as $VAR in the child process only.",
|
|
165
|
+
description: "Run a command with a secret's value injected into an env var: `akm secret run <ref> <VAR> -- <command>`. The value is set as $VAR in the child process only. Pass --clean to start the child with a minimal inherited environment instead of the full parent environment.",
|
|
156
166
|
},
|
|
157
167
|
args: {
|
|
158
168
|
ref: { type: "positional", description: "Secret ref", required: true },
|
|
159
169
|
var: { type: "positional", description: "Environment variable name to inject the value into", required: true },
|
|
170
|
+
clean: {
|
|
171
|
+
type: "boolean",
|
|
172
|
+
description: "Start the child with a minimal inherited environment (PATH/HOME/locale/terminal basics) instead of the full parent environment.",
|
|
173
|
+
default: false,
|
|
174
|
+
},
|
|
175
|
+
inherit: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: "When used with --clean, also inherit these parent env vars (comma-separated). Ignored without --clean.",
|
|
178
|
+
},
|
|
160
179
|
},
|
|
161
180
|
run({ args }) {
|
|
162
181
|
return runWithJsonErrors(async () => {
|
|
@@ -181,7 +200,10 @@ const secretRunCommand = defineCommand({
|
|
|
181
200
|
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
182
201
|
}
|
|
183
202
|
const { readValue } = await import("./secret.js");
|
|
184
|
-
const mergedEnv =
|
|
203
|
+
const mergedEnv = buildChildEnv(process.env, {
|
|
204
|
+
clean: getHyphenatedArg(args, "clean") === true,
|
|
205
|
+
inherit: parseKeyListFlag(getHyphenatedArg(args, "inherit")) ?? [],
|
|
206
|
+
});
|
|
185
207
|
mergedEnv[varName] = readValue(absPath).toString("utf8");
|
|
186
208
|
// Audit trail: record access by ref + var name only — never the value.
|
|
187
209
|
appendEvent({
|
|
@@ -316,7 +316,7 @@ export const HEALTH_CHECKS = [
|
|
|
316
316
|
status: aa.validationFailed > 0 ? "warn" : "pass",
|
|
317
317
|
confidence: aa.promoted + aa.validationFailed > 0 ? "high" : "low",
|
|
318
318
|
message: aa.validationFailed > 0
|
|
319
|
-
? `${aa.validationFailed}
|
|
319
|
+
? `${aa.validationFailed} auto-accept validation attempt(s) failed after passing the confidence threshold (truncated description, invalid frontmatter, etc.) — the affected proposals remain pending for manual review.`
|
|
320
320
|
: aa.promoted > 0
|
|
321
321
|
? `Auto-accept healthy: ${aa.promoted} proposal(s) promoted, 0 validation failures.`
|
|
322
322
|
: "Auto-accept gate did not run (disabled or no proposals above threshold).",
|
|
@@ -5,7 +5,12 @@ import { loadConfig } from "../../core/config/config.js";
|
|
|
5
5
|
import { appendEvent } from "../../core/events.js";
|
|
6
6
|
import { getPhaseThreshold, withStateDb } from "../../core/state-db.js";
|
|
7
7
|
import { info, warn } from "../../core/warn.js";
|
|
8
|
-
import { promoteProposal, recordGateDecision } from "../proposal/validators/proposals.js";
|
|
8
|
+
import { getProposal, promoteProposal, recordGateDecision } from "../proposal/validators/proposals.js";
|
|
9
|
+
async function sha256Hex(input) {
|
|
10
|
+
const data = new TextEncoder().encode(input);
|
|
11
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
12
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
13
|
+
}
|
|
9
14
|
/**
|
|
10
15
|
* Derive a stable, low-cardinality reason bucket from an auto-accept promotion
|
|
11
16
|
* error. `promoteProposal` throws a `validateProposal` report formatted as
|
|
@@ -36,7 +41,14 @@ function classifyPromoteFailure(err) {
|
|
|
36
41
|
* @param promoteFn Injectable override for `promoteProposal` (test seam).
|
|
37
42
|
*/
|
|
38
43
|
export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProposal) {
|
|
39
|
-
const result = {
|
|
44
|
+
const result = {
|
|
45
|
+
promoted: [],
|
|
46
|
+
skipped: [],
|
|
47
|
+
failed: [],
|
|
48
|
+
suppressed: [],
|
|
49
|
+
failedByReason: {},
|
|
50
|
+
failedBySource: {},
|
|
51
|
+
};
|
|
40
52
|
// --- Guard: gate is disabled or context is incomplete ---
|
|
41
53
|
if (cfg.dryRun || cfg.globalThreshold === undefined || !cfg.stashDir) {
|
|
42
54
|
result.skipped = candidates.map((c) => c.proposalId);
|
|
@@ -70,6 +82,14 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
|
|
|
70
82
|
};
|
|
71
83
|
for (const candidate of candidates) {
|
|
72
84
|
const { proposalId, confidence } = candidate;
|
|
85
|
+
let currentProposal;
|
|
86
|
+
try {
|
|
87
|
+
currentProposal = cfg.stashDir ? getProposal(cfg.stashDir, proposalId) : undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
currentProposal = undefined;
|
|
91
|
+
}
|
|
92
|
+
const currentContentHash = currentProposal ? await sha256Hex(currentProposal.payload.content) : undefined;
|
|
73
93
|
// Determine if this candidate is exploration-eligible: below-threshold
|
|
74
94
|
// (would normally be deferred) but with a valid confidence score and budget
|
|
75
95
|
// remaining. No-confidence candidates are never exploration-promoted.
|
|
@@ -90,6 +110,12 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
|
|
|
90
110
|
if (isExploration)
|
|
91
111
|
explorationRemaining -= 1;
|
|
92
112
|
const promoteReason = isExploration ? "exploration-budget" : "above-threshold";
|
|
113
|
+
if (currentProposal?.gateDecision?.outcome === "auto-rejected" &&
|
|
114
|
+
currentProposal.gateDecision.contentHash !== undefined &&
|
|
115
|
+
currentProposal.gateDecision.contentHash === currentContentHash) {
|
|
116
|
+
result.suppressed.push(proposalId);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
93
119
|
try {
|
|
94
120
|
const promotion = await promoteFn(cfg.stashDir, resolvedConfig, proposalId, {}, undefined);
|
|
95
121
|
stamp(promotion.proposal.id, {
|
|
@@ -97,6 +123,7 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
|
|
|
97
123
|
reason: promoteReason,
|
|
98
124
|
confidence,
|
|
99
125
|
thresholds: { autoAccept: effectiveThreshold },
|
|
126
|
+
...(currentContentHash !== undefined ? { contentHash: currentContentHash } : {}),
|
|
100
127
|
gate: gateLabel,
|
|
101
128
|
});
|
|
102
129
|
// Resolve the eligibilitySource: exploration-promoted proposals get
|
|
@@ -146,6 +173,7 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
|
|
|
146
173
|
reason,
|
|
147
174
|
confidence,
|
|
148
175
|
thresholds: { autoAccept: effectiveThreshold },
|
|
176
|
+
...(currentContentHash !== undefined ? { contentHash: currentContentHash } : {}),
|
|
149
177
|
gate: gateLabel,
|
|
150
178
|
});
|
|
151
179
|
// If exploration budget was consumed but promotion failed, restore the slot
|
|
@@ -221,7 +221,7 @@ export const improveCommand = defineCommand({
|
|
|
221
221
|
runRecorded = true; // Suppress any late signal-handler write — the success path owns the row now.
|
|
222
222
|
if (primaryStashDir) {
|
|
223
223
|
try {
|
|
224
|
-
writeImproveResultFile(primaryStashDir, runId, improveResult, startedAtIso);
|
|
224
|
+
writeImproveResultFile(primaryStashDir, runId, improveResult, startedAtIso, profileArg ?? null);
|
|
225
225
|
}
|
|
226
226
|
catch (err) {
|
|
227
227
|
// Stderr warning on the failure path is preferable to crashing
|
|
@@ -72,8 +72,15 @@ export function relativeImproveResultPath(runId) {
|
|
|
72
72
|
* dry-run column is indexed so productivity audits can filter cleanly
|
|
73
73
|
* (closes the dry-run/real-run artifact-trap recorded in MEMORY.md
|
|
74
74
|
* `feedback_akm_dryrun_artifact_trap`).
|
|
75
|
+
*
|
|
76
|
+
* @param profile - The `--profile` value passed to this invocation (e.g.
|
|
77
|
+
* `quick`, `reflect-distill`), or `null`/`undefined` when no profile was
|
|
78
|
+
* given. Mirrors {@link recordTerminatedImproveRun}'s `ctx.profile`
|
|
79
|
+
* convention so successful and terminated runs are equally queryable by
|
|
80
|
+
* profile. Previously hardcoded to `null` here, which meant only
|
|
81
|
+
* abnormally-terminated runs recorded their profile in state.db.
|
|
75
82
|
*/
|
|
76
|
-
export function writeImproveResultFile(stashDir, runId, result, startedAt) {
|
|
83
|
+
export function writeImproveResultFile(stashDir, runId, result, startedAt, profile) {
|
|
77
84
|
withStateDb((db) => {
|
|
78
85
|
const completedAt = new Date().toISOString();
|
|
79
86
|
// startedAt is the ISO timestamp captured at process launch (passed from the
|
|
@@ -87,7 +94,7 @@ export function writeImproveResultFile(stashDir, runId, result, startedAt) {
|
|
|
87
94
|
completedAt,
|
|
88
95
|
stashDir,
|
|
89
96
|
dryRun: Boolean(result.dryRun),
|
|
90
|
-
profile: null,
|
|
97
|
+
profile: profile ?? null,
|
|
91
98
|
scopeMode: result.scope?.mode ?? "all",
|
|
92
99
|
scopeValue: result.scope?.value ?? null,
|
|
93
100
|
guidance: result.guidance ?? null,
|