akm-cli 0.9.0-beta.50 → 0.9.0-beta.51

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 (48) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/wiki/ingest-workflow-template.md +27 -6
  6. package/dist/cli/parse-args.js +46 -1
  7. package/dist/cli.js +12 -6
  8. package/dist/commands/config-cli.js +18 -2
  9. package/dist/commands/env/child-env.js +47 -0
  10. package/dist/commands/env/env-cli.js +17 -2
  11. package/dist/commands/env/secret-cli.js +24 -2
  12. package/dist/commands/health/checks.js +1 -1
  13. package/dist/commands/improve/improve-auto-accept.js +30 -2
  14. package/dist/commands/improve/improve-cli.js +1 -1
  15. package/dist/commands/improve/improve-result-file.js +9 -2
  16. package/dist/commands/improve/recombine.js +52 -15
  17. package/dist/commands/lint/env-key-rules.js +4 -0
  18. package/dist/commands/read/knowledge.js +5 -2
  19. package/dist/commands/read/search-cli.js +2 -4
  20. package/dist/commands/read/search.js +9 -6
  21. package/dist/commands/read/show.js +19 -5
  22. package/dist/commands/sources/init.js +13 -8
  23. package/dist/commands/sources/installed-stashes.js +6 -2
  24. package/dist/commands/sources/schema-repair.js +33 -47
  25. package/dist/commands/sources/source-add.js +7 -3
  26. package/dist/commands/tasks/tasks.js +38 -10
  27. package/dist/core/asset/asset-registry.js +1 -1
  28. package/dist/core/asset/asset-spec.js +4 -2
  29. package/dist/core/config/config-migration.js +12 -11
  30. package/dist/indexer/passes/memory-inference.js +3 -2
  31. package/dist/indexer/search/db-search.js +6 -4
  32. package/dist/indexer/search/search-source.js +15 -2
  33. package/dist/integrations/agent/prompts.js +1 -1
  34. package/dist/llm/memory-infer-impl.js +138 -0
  35. package/dist/llm/memory-infer.js +1 -135
  36. package/dist/migrate-storage-node.mjs +8 -0
  37. package/dist/output/renderers.js +1 -1
  38. package/dist/scripts/migrate-storage.js +463 -347
  39. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +99 -99
  40. package/dist/sources/include.js +6 -2
  41. package/dist/sources/providers/git-install.js +10 -6
  42. package/dist/sources/providers/provider-utils.js +13 -7
  43. package/dist/sources/providers/website.js +8 -3
  44. package/dist/sources/website-ingest.js +136 -20
  45. package/dist/text-import-hook.mjs +0 -0
  46. package/docs/data-and-telemetry.md +2 -2
  47. package/docs/migration/release-notes/0.9.0.md +39 -0
  48. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.0] — 2026-06-30
10
+
9
11
  ### Fixed
10
12
 
11
13
  - **improve/recombine: cap-aware decay — the `maxClustersPerRun` cap no longer
package/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
  [![license](https://img.shields.io/github/license/itlackey/akm)](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, vaults, lessons, and scheduled
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.8 requires the prebuilt binary or the Bun runtime. Node.js / npm / pnpm are not supported in 0.8.0** running `npm install -g akm-cli` on a Node.js-only machine will print an error from the preinstall hook and exit without installing. Cross-runtime support (Node, npm, pnpm) is planned for 0.9.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, vaults, lessons, and scheduled tasks
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,11 +20,31 @@ 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. If there are no `uncited-raw` findings, exit cleanly
24
- after a final `akm index` + `akm wiki lint {{WIKI_NAME}}` verification.
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
- 3. **For each pending raw file, read the source and find related pages.**
27
- Open the raw file directly from `{{WIKI_DIR}}/raw/`, then search:
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
  ```
@@ -46,8 +66,9 @@ empty and the caller explicitly asked for interactive ingest.
46
66
  6. **Update xrefs both ways.** If page A now xrefs page B, page B must xref
47
67
  page A. `akm wiki lint {{WIKI_NAME}}` will flag violations.
48
68
 
49
- 7. **Append to `log.md`.** One entry per ingested raw source: date, raw slug,
50
- one-line summary, refs to created/edited pages. Newest at the top.
69
+ 7. **Append to `log.md`.** One entry per processed raw source (ingested or
70
+ skipped-as-unprocessable): date, raw slug, one-line summary, refs to
71
+ created/edited pages. Newest at the top.
51
72
 
52
73
  8. **Regenerate the index + verify.**
53
74
  ```sh
@@ -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
- // ── Subcommand detection ─────────────────────────────────────────────────────
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 = Number.parseInt((process.versions.node ?? "0").split(".")[0], 10);
17
- if (Number.isNaN(major) || major < 20) {
18
- console.error("\n ERROR: akm-cli requires the Bun runtime (https://bun.sh) or Node.js >= 20.\n" +
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
- if (getOutputMode().shape === "summary" && process.argv[2] !== "show") {
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
- return configGet(config, k);
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 = { ...process.env };
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 = { ...process.env };
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} proposal(s) passed confidence threshold but failed auto-accept validation (truncated description, invalid frontmatter, etc.) — they remain in the queue for manual review.`
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 = { promoted: [], skipped: [], failed: [], failedByReason: {} };
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,