akm-cli 0.9.14 → 0.9.15-beta.1
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 +397 -0
- package/STABILITY.md +6 -3
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -0
- package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -0
- package/dist/assets/prompts/reflect-llm-schema-contract.md +2 -0
- package/dist/assets/tasks/core/improve.yml +1 -1
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +1 -1
- package/dist/cli/retired-commands.js +0 -1
- package/dist/cli/shared.js +9 -0
- package/dist/cli/unknown-flags.js +1 -0
- package/dist/cli.js +3 -2
- package/dist/commands/config-cli.js +85 -3
- package/dist/commands/env/env-cli.js +1 -42
- package/dist/commands/env/env.js +1 -1
- package/dist/commands/env/secret-cli.js +1 -2
- package/dist/commands/health/checks.js +357 -63
- package/dist/commands/health/engine-usage.js +45 -0
- package/dist/commands/health/improve-metrics.js +18 -0
- package/dist/commands/health/llm-usage.js +41 -1
- package/dist/commands/health/plugin-staleness.js +7 -3
- package/dist/commands/health/version-drift.js +93 -0
- package/dist/commands/health/windows.js +3 -1
- package/dist/commands/health.js +44 -9
- package/dist/commands/improve/consolidate/chunking.js +4 -2
- package/dist/commands/improve/improve-cli.js +99 -5
- package/dist/commands/improve/improve-report.js +154 -0
- package/dist/commands/improve/improve-result-file.js +45 -33
- package/dist/commands/improve/improve-strategies.js +133 -3
- package/dist/commands/improve/improve-usage-report.js +182 -0
- package/dist/commands/improve/improve.js +40 -3
- package/dist/commands/improve/locks.js +27 -78
- package/dist/commands/improve/planner.js +1 -0
- package/dist/commands/improve/preparation.js +9 -1
- package/dist/commands/improve/reflect.js +44 -4
- package/dist/commands/models-cli.js +50 -1
- package/dist/commands/proposal/repository.js +8 -3
- package/dist/commands/proposal/validators/proposal-quality-validators.js +41 -6
- package/dist/commands/proposal/validators/proposal-validators.js +24 -0
- package/dist/commands/read/search-cli.js +38 -2
- package/dist/commands/read/show.js +103 -4
- package/dist/commands/sources/info.js +5 -1
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/stash-cli.js +31 -0
- package/dist/commands/tasks/tasks-cli.js +49 -2
- package/dist/commands/workflow-cli.js +86 -12
- package/dist/core/asset/markdown-fragments.js +35 -0
- package/dist/core/config/config-schema.js +14 -0
- package/dist/core/config/config.js +302 -24
- package/dist/core/env-secret-ref.js +58 -5
- package/dist/core/errors.js +30 -0
- package/dist/core/improve-result.js +51 -0
- package/dist/core/loopback.js +17 -0
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +96 -0
- package/dist/core/sensitive-marker-path.js +19 -0
- package/dist/core/state-db.js +74 -14
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-writer-lock.js +40 -1
- package/dist/indexer/index-written-assets.js +21 -1
- package/dist/indexer/indexer.js +18 -17
- package/dist/indexer/materialize-embeddings.js +282 -32
- package/dist/indexer/search/db-search.js +49 -2
- package/dist/integrations/agent/engine-resolution.js +96 -6
- package/dist/integrations/agent/execution-definitions.js +6 -15
- package/dist/integrations/agent/execution-lowering.js +6 -1
- package/dist/integrations/agent/execution-preparation.js +1 -1
- package/dist/integrations/agent/model-map.js +123 -20
- package/dist/integrations/agent/prompts.js +40 -8
- package/dist/integrations/agent/runner-dispatch.js +9 -3
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/llm/client.js +8 -3
- package/dist/llm/embedder.js +20 -8
- package/dist/llm/embedders/local.js +10 -2
- package/dist/llm/embedders/remote.js +188 -21
- package/dist/output/shapes/helpers.js +38 -2
- package/dist/output/shapes/models-list.js +16 -0
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/shapes.js +4 -0
- package/dist/output/text/command-format.js +29 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/improve-report.js +27 -0
- package/dist/{commands/env/marker-path.js → output/text/models.js} +4 -3
- package/dist/output/text/show-format.js +4 -0
- package/dist/output/text.js +4 -0
- package/dist/scripts/akm-migrate-node.js +24798 -21732
- package/dist/scripts/akm-migrate.js +23408 -20343
- package/dist/storage/repositories/improve-runs-repository.js +34 -0
- package/dist/storage/repositories/index-fts-repository.js +49 -6
- package/dist/storage/repositories/index-vec-repository.js +30 -0
- package/dist/storage/repositories/workflow-runs-repository.js +55 -18
- package/dist/tasks/backends/cron.js +14 -7
- package/dist/tasks/run/run-workflow-task.js +16 -0
- package/dist/workflows/exec/child-workflow.js +2 -2
- package/dist/workflows/exec/dispatch-redaction.js +21 -9
- package/dist/workflows/exec/run-workflow.js +6 -5
- package/dist/workflows/runtime/runs.js +33 -5
- package/docs/migration/release-notes/0.9.15.md +52 -0
- package/docs/migration/release-notes/README.md +4 -0
- package/docs/reference/cli.md +245 -29
- package/docs/reference/configuration.md +180 -19
- package/docs/reference/data-and-telemetry.md +8 -0
- package/docs/reference/tasks.md +16 -1
- package/docs/reference/workflow-schema.md +5 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +8 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* `engine-last-used` support for `akm health` (#950).
|
|
6
|
+
*
|
|
7
|
+
* An engine bound to an enabled improve process that has not actually been
|
|
8
|
+
* invoked in weeks looks identical to a healthy one on every existing check —
|
|
9
|
+
* `default-llm-engine`/`configured-engines` only probe availability/reachability,
|
|
10
|
+
* never USE. This module answers "when did this engine last run, and for
|
|
11
|
+
* whom" by folding `llm_usage` events over a fixed lookback window,
|
|
12
|
+
* independent of the report's `--since` (mirrors `session-extraction`'s
|
|
13
|
+
* independent `SESSION_EXTRACTION_LEDGER_WINDOW_DAYS` window in `checks.ts`).
|
|
14
|
+
*/
|
|
15
|
+
import { daysToMs } from "../../core/common.js";
|
|
16
|
+
import { readEvents } from "../../core/events.js";
|
|
17
|
+
import { LLM_USAGE_EVENT } from "../../llm/usage-persist.js";
|
|
18
|
+
import { decodeLlmUsageRecord } from "../../llm/usage-telemetry.js";
|
|
19
|
+
/** Rolling window `engine-last-used` reads over, independent of `--since`. */
|
|
20
|
+
export const ENGINE_LAST_USED_LOOKBACK_DAYS = 30;
|
|
21
|
+
/** ISO cutoff for the `ENGINE_LAST_USED_LOOKBACK_DAYS` window, ending at `now()`. */
|
|
22
|
+
export function engineLastUsedSince(now) {
|
|
23
|
+
return new Date(now() - daysToMs(ENGINE_LAST_USED_LOOKBACK_DAYS)).toISOString();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fold `llm_usage` events over the lookback window into the most recent call
|
|
27
|
+
* per engine. Best-effort like every other health probe: an event with no
|
|
28
|
+
* `engine` field (pre-#576 rows, or a call outside any resolved engine) is
|
|
29
|
+
* skipped rather than bucketed under a synthetic key.
|
|
30
|
+
*/
|
|
31
|
+
export function readLastEngineUsage(stateDbPath, now) {
|
|
32
|
+
const since = engineLastUsedSince(now);
|
|
33
|
+
const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath }).events;
|
|
34
|
+
const lastUsed = new Map();
|
|
35
|
+
for (const event of events) {
|
|
36
|
+
const record = decodeLlmUsageRecord(event.metadata);
|
|
37
|
+
if (!record?.engine)
|
|
38
|
+
continue;
|
|
39
|
+
const existing = lastUsed.get(record.engine);
|
|
40
|
+
if (!existing || event.ts > existing.lastUsedAt) {
|
|
41
|
+
lastUsed.set(record.engine, { lastUsedAt: event.ts, process: record.process });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return lastUsed;
|
|
45
|
+
}
|
|
@@ -44,6 +44,24 @@ export function taskFailureDetail(row) {
|
|
|
44
44
|
export function isAgentTaskHistoryRow(row) {
|
|
45
45
|
return row.target_kind === "command";
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* #943: reason-value breakdown for a set of agent (command-kind) task
|
|
49
|
+
* failure rows — how much of the observed failures are `timeout` vs
|
|
50
|
+
* `non_zero_exit` vs `spawn_failed` etc., so `akm health`'s `task-fail-rate`
|
|
51
|
+
* advisory can say "timeout-dominant" from data rather than log grep. Keeps
|
|
52
|
+
* the existing `AgentFailureReason` vocabulary verbatim (spawn.ts) — a
|
|
53
|
+
* reason-per-row read failure (already warned by {@link taskFailureDetail})
|
|
54
|
+
* still counts under `"unknown"` rather than being dropped, so the total
|
|
55
|
+
* always equals `agentFailures.length`.
|
|
56
|
+
*/
|
|
57
|
+
export function countAgentFailureReasons(agentFailures) {
|
|
58
|
+
const counts = {};
|
|
59
|
+
for (const row of agentFailures) {
|
|
60
|
+
const reason = String(taskFailureDetail(row)?.reason ?? "unknown");
|
|
61
|
+
counts[reason] = (counts[reason] ?? 0) + 1;
|
|
62
|
+
}
|
|
63
|
+
return counts;
|
|
64
|
+
}
|
|
47
65
|
function createUnknownImproveMetrics() {
|
|
48
66
|
return {
|
|
49
67
|
invoked: 0,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { readEvents } from "../../core/events.js";
|
|
9
9
|
import { LLM_USAGE_EVENT } from "../../llm/usage-persist.js";
|
|
10
10
|
import { decodeLlmUsageRecord } from "../../llm/usage-telemetry.js";
|
|
11
|
-
/** Stage key used for `llm_usage` events recorded
|
|
11
|
+
/** Stage/process/engine/model key used for `llm_usage` events recorded with no value for that dimension. */
|
|
12
12
|
const UNATTRIBUTED_STAGE = "unattributed";
|
|
13
13
|
function emptyLlmUsageStageAggregate() {
|
|
14
14
|
return {
|
|
@@ -18,6 +18,7 @@ function emptyLlmUsageStageAggregate() {
|
|
|
18
18
|
completionTokens: 0,
|
|
19
19
|
totalTokens: 0,
|
|
20
20
|
reasoningTokens: 0,
|
|
21
|
+
failures: 0,
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
/** A zeroed aggregate — also the value health reports when it could not read state.db at all (#791). */
|
|
@@ -52,6 +53,8 @@ export function summarizeLlmUsage(events) {
|
|
|
52
53
|
target.completionTokens += record.completionTokens ?? 0;
|
|
53
54
|
target.totalTokens += record.totalTokens ?? 0;
|
|
54
55
|
target.reasoningTokens += record.reasoningTokens ?? 0;
|
|
56
|
+
if (record.outcome === "error")
|
|
57
|
+
target.failures += 1;
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
return aggregate;
|
|
@@ -64,3 +67,40 @@ export function readLlmUsageAggregate(stateDbPath, since, until) {
|
|
|
64
67
|
});
|
|
65
68
|
return summarizeLlmUsage(events);
|
|
66
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Aggregate `llm_usage` events (#576) into a process x engine x model
|
|
72
|
+
* cross-tab (#944) — one row per distinct `(process, engine, model)` triple
|
|
73
|
+
* seen, each carrying the same call/failure/token/duration totals as
|
|
74
|
+
* {@link LlmUsageStageAggregate}. A call missing any one of the three
|
|
75
|
+
* dimensions is keyed under {@link UNATTRIBUTED_STAGE} for that dimension
|
|
76
|
+
* (never dropped). Does not fold into, or change the shape of,
|
|
77
|
+
* {@link summarizeLlmUsage}'s existing `byStage`/`byProcess`/`byEngine`
|
|
78
|
+
* breakdowns — `akm health`'s existing consumers of those stay untouched.
|
|
79
|
+
* Row order is insertion order (first `(process, engine, model)` triple seen).
|
|
80
|
+
*/
|
|
81
|
+
export function summarizeLlmUsageCrossTab(events) {
|
|
82
|
+
const rows = new Map();
|
|
83
|
+
for (const event of events) {
|
|
84
|
+
const record = decodeLlmUsageRecord(event.metadata);
|
|
85
|
+
if (!record)
|
|
86
|
+
continue;
|
|
87
|
+
const process = record.process ?? UNATTRIBUTED_STAGE;
|
|
88
|
+
const engine = record.engine ?? UNATTRIBUTED_STAGE;
|
|
89
|
+
const model = record.model ?? UNATTRIBUTED_STAGE;
|
|
90
|
+
const key = `${process}:${engine}:${model}`;
|
|
91
|
+
let row = rows.get(key);
|
|
92
|
+
if (!row) {
|
|
93
|
+
row = { process, engine, model, ...emptyLlmUsageStageAggregate() };
|
|
94
|
+
rows.set(key, row);
|
|
95
|
+
}
|
|
96
|
+
row.calls += 1;
|
|
97
|
+
row.totalDurationMs += record.durationMs;
|
|
98
|
+
row.promptTokens += record.promptTokens ?? 0;
|
|
99
|
+
row.completionTokens += record.completionTokens ?? 0;
|
|
100
|
+
row.totalTokens += record.totalTokens ?? 0;
|
|
101
|
+
row.reasoningTokens += record.reasoningTokens ?? 0;
|
|
102
|
+
if (record.outcome === "error")
|
|
103
|
+
row.failures += 1;
|
|
104
|
+
}
|
|
105
|
+
return [...rows.values()];
|
|
106
|
+
}
|
|
@@ -29,15 +29,19 @@
|
|
|
29
29
|
* and there was previously no way to know that from the CLI side.
|
|
30
30
|
*
|
|
31
31
|
* Read-only: this never fetches, writes, or mutates the plugin cache or
|
|
32
|
-
* marketplace clone. Check 2 is
|
|
33
|
-
* health` makes no network call" (see `./health-advisories.md
|
|
32
|
+
* marketplace clone. Check 2 is one of two deliberate exceptions to "`akm
|
|
33
|
+
* health` makes no network call" (see `./health-advisories.md`; the other is
|
|
34
|
+
* the `cli-version` advisory in `./version-drift.ts`, #950): a plugin's
|
|
34
35
|
* local marketplace clone is not proof of what is newest upstream — the
|
|
35
36
|
* incident above involved a clone that hadn't seen the fix's tag at all — so
|
|
36
37
|
* the only way to ever detect drift is to ask the remote what tags exist.
|
|
37
38
|
* That query is a `git ls-remote --tags` (lists refs; fetches nothing,
|
|
38
39
|
* writes nothing) with a short timeout, and any failure (offline, no
|
|
39
40
|
* remote, timeout) degrades to "installed version reported, no staleness
|
|
40
|
-
* claim" rather than a false positive or a hang.
|
|
41
|
+
* claim" rather than a false positive or a hang. Unlike `cli-version`, this
|
|
42
|
+
* check is NOT gated behind `--probe`/`--no-probe` — it predates that flag
|
|
43
|
+
* and stays unconditional; see `./version-drift.ts` for why the newer check
|
|
44
|
+
* chose the opposite default.
|
|
41
45
|
*
|
|
42
46
|
* Every collector here is best-effort and silent on missing/unreadable
|
|
43
47
|
* input: no Claude plugin installed, no marketplace clone, an unreadable
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* `cli-version` advisory for `akm health` (#950).
|
|
6
|
+
*
|
|
7
|
+
* A fleet running a mix of akm-cli versions can look healthy on every other
|
|
8
|
+
* check while quietly running stale code (the motivating case: a host on
|
|
9
|
+
* 0.9.12 next to four peers already on 0.9.14, with nothing in `akm health`
|
|
10
|
+
* calling that out). This closes that gap by reusing {@link checkForUpdate}
|
|
11
|
+
* — the exact "installed vs GitHub latest release" comparison `akm upgrade`
|
|
12
|
+
* already trusts — rather than adding a second, parallel "ask npm" query
|
|
13
|
+
* that could disagree with it during the gap between a GitHub release and
|
|
14
|
+
* its npm publish step.
|
|
15
|
+
*
|
|
16
|
+
* Modelled 1:1 on `plugin-staleness.ts`'s shape: an injectable network seam,
|
|
17
|
+
* best-effort, silent-on-failure (never a false positive, never a hang — the
|
|
18
|
+
* network call is bounded to {@link CLI_VERSION_CHECK_TIMEOUT_MS} with no
|
|
19
|
+
* retries, not `checkForUpdate`'s own 30s/3-retry defaults, which are tuned
|
|
20
|
+
* for the deliberate, explicit `akm upgrade` command rather than a
|
|
21
|
+
* `--probe`-default-on health check). This is the SECOND deliberate network
|
|
22
|
+
* exception in `akm health` (see
|
|
23
|
+
* `plugin-staleness.ts` and `docs/architecture/internals/health-advisories.md`)
|
|
24
|
+
* — unlike plugin-staleness's unconditional `git ls-remote`, this one is
|
|
25
|
+
* gated behind the same `--probe`/`--no-probe` flag the engine-reachability
|
|
26
|
+
* checks already use, so an air-gapped host's existing `--no-probe` habit
|
|
27
|
+
* suppresses it too, and offline/rate-limited failures degrade to `unknown`
|
|
28
|
+
* (never a false `warn`).
|
|
29
|
+
*/
|
|
30
|
+
import { pkgVersion } from "../../version.js";
|
|
31
|
+
import { checkForUpdate } from "../sources/self-update.js";
|
|
32
|
+
/**
|
|
33
|
+
* Bound on the `checkForUpdate` network call. `checkForUpdate`'s own
|
|
34
|
+
* defaults (30s timeout, up to 3 retries with exponential backoff) are
|
|
35
|
+
* tuned for `akm upgrade` — a deliberate, explicit human command where a
|
|
36
|
+
* long wait is acceptable. `akm health --probe` defaults to `true` and is
|
|
37
|
+
* not such a gate, so this advisory overrides both to match the bounded,
|
|
38
|
+
* single-attempt discipline every other network-touching health check uses
|
|
39
|
+
* (`plugin-staleness.ts`'s 5s `git ls-remote`, `probeLlmEndpoint`'s 3s
|
|
40
|
+
* `AbortSignal.timeout`): a stale or air-gapped host must degrade to
|
|
41
|
+
* `unknown` in seconds, never block `akm health` for minutes.
|
|
42
|
+
*/
|
|
43
|
+
const CLI_VERSION_CHECK_TIMEOUT_MS = 5_000;
|
|
44
|
+
/**
|
|
45
|
+
* Build the `cli-version` advisory. `probe` mirrors the engine-reachability
|
|
46
|
+
* checks' `--probe`/`--no-probe` gating: only network when `true`; otherwise
|
|
47
|
+
* `unknown` with "not probed", matching the un-probed engine-reachability
|
|
48
|
+
* message shape rather than silently omitting the check.
|
|
49
|
+
*/
|
|
50
|
+
export async function collectVersionDriftAdvisory(probe, deps = {}) {
|
|
51
|
+
const cliVersion = deps.cliVersion ?? pkgVersion;
|
|
52
|
+
if (!probe) {
|
|
53
|
+
return {
|
|
54
|
+
name: "cli-version",
|
|
55
|
+
kind: "deterministic",
|
|
56
|
+
status: "unknown",
|
|
57
|
+
confidence: "high",
|
|
58
|
+
message: `akm v${cliVersion} is installed. Version-drift was not probed.`,
|
|
59
|
+
evidence: { installedVersion: cliVersion },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const result = await (deps.checkForUpdate ?? checkForUpdate)(cliVersion, {
|
|
64
|
+
timeout: CLI_VERSION_CHECK_TIMEOUT_MS,
|
|
65
|
+
retries: 0,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
name: "cli-version",
|
|
69
|
+
kind: "deterministic",
|
|
70
|
+
status: result.updateAvailable ? "warn" : "pass",
|
|
71
|
+
confidence: "high",
|
|
72
|
+
message: result.updateAvailable
|
|
73
|
+
? `akm v${cliVersion} is installed; v${result.latestVersion} is available — run 'akm upgrade'.`
|
|
74
|
+
: `akm v${cliVersion} is installed and up to date.`,
|
|
75
|
+
evidence: { installedVersion: cliVersion, latestVersion: result.latestVersion },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// Offline, rate-limited, or a malformed release response — never a false
|
|
80
|
+
// "stale" or "up to date" claim, only "could not be checked".
|
|
81
|
+
return {
|
|
82
|
+
name: "cli-version",
|
|
83
|
+
kind: "deterministic",
|
|
84
|
+
status: "unknown",
|
|
85
|
+
confidence: "high",
|
|
86
|
+
message: `akm v${cliVersion} is installed; the update check could not reach the release source.`,
|
|
87
|
+
evidence: {
|
|
88
|
+
installedVersion: cliVersion,
|
|
89
|
+
error: error instanceof Error ? error.constructor.name : "UnknownError",
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -11,7 +11,7 @@ import { readEvents } from "../../core/events.js";
|
|
|
11
11
|
import { buildTaskRunId, getLoggedRunIds } from "../../core/logs-db.js";
|
|
12
12
|
import { DURATION_UNITS, parseDuration } from "../../core/time.js";
|
|
13
13
|
import { queryTaskHistory } from "../../storage/repositories/task-history-repository.js";
|
|
14
|
-
import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow, roundRate, summarizeImproveCompleted, summarizeImproveRuns, taskFailureDetail, } from "./improve-metrics.js";
|
|
14
|
+
import { buildImproveSkipSummary, computeWallTimeStats, countAgentFailureReasons, isAgentTaskHistoryRow, roundRate, summarizeImproveCompleted, summarizeImproveRuns, taskFailureDetail, } from "./improve-metrics.js";
|
|
15
15
|
import { readLlmUsageAggregate } from "./llm-usage.js";
|
|
16
16
|
import { computeDegradationMetrics, computeDenominatorFixedCoverage } from "./metrics.js";
|
|
17
17
|
import { buildPerRunSummaries } from "./task-runs.js";
|
|
@@ -156,6 +156,7 @@ export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Da
|
|
|
156
156
|
const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
|
|
157
157
|
const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
|
|
158
158
|
const agentFailureRate = agentRows.length === 0 ? 0 : agentFailures.length / agentRows.length;
|
|
159
|
+
const agentFailureReasonCounts = countAgentFailureReasons(agentFailures);
|
|
159
160
|
const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime()).length;
|
|
160
161
|
const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
|
|
161
162
|
const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
|
|
@@ -185,6 +186,7 @@ export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Da
|
|
|
185
186
|
const metrics = {
|
|
186
187
|
taskFailRate: roundRate(taskFailRate),
|
|
187
188
|
agentFailureRate: roundRate(agentFailureRate),
|
|
189
|
+
agentFailureReasonCounts,
|
|
188
190
|
stuckActiveRuns,
|
|
189
191
|
logBackingRate: roundRate(logBackingRate),
|
|
190
192
|
probeRoundTripMs: null,
|
package/dist/commands/health.js
CHANGED
|
@@ -14,14 +14,16 @@ import { listExistingTableNames, listPendingStateMigrations, openStateDatabase }
|
|
|
14
14
|
import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
|
|
15
15
|
import { probeLlmEndpoint } from "../llm/client.js";
|
|
16
16
|
import { getExtractOutcomeCountsSince } from "../storage/repositories/extract-sessions-repository.js";
|
|
17
|
+
import { countImproveRunsSince } from "../storage/repositories/improve-runs-repository.js";
|
|
17
18
|
import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
18
19
|
import { getAllEntries } from "../storage/repositories/index-entries-repository.js";
|
|
19
20
|
import { queryTaskHistory } from "../storage/repositories/task-history-repository.js";
|
|
20
21
|
import { pkgVersion } from "../version.js";
|
|
21
22
|
import { collectImproveAdvisories } from "./health/advisories.js";
|
|
22
|
-
import { HEALTH_CHECKS, runHealthEngineProbes, runPendingStateMigrationsCheck, SESSION_EXTRACTION_LEDGER_WINDOW_DAYS, } from "./health/checks.js";
|
|
23
|
+
import { HEALTH_CHECKS, probeActiveImproveStrategy, runHealthEngineProbes, runPendingStateMigrationsCheck, SESSION_EXTRACTION_LEDGER_WINDOW_DAYS, } from "./health/checks.js";
|
|
23
24
|
import { collectDataDirUsageAdvisory } from "./health/data-dir-usage.js";
|
|
24
|
-
import {
|
|
25
|
+
import { engineLastUsedSince, readLastEngineUsage } from "./health/engine-usage.js";
|
|
26
|
+
import { buildImproveSkipSummary, computeWallTimeStats, countAgentFailureReasons, isAgentTaskHistoryRow, roundRate, summarizeImproveCompleted, summarizeImproveRuns, taskFailureDetail, } from "./health/improve-metrics.js";
|
|
25
27
|
import { emptyLlmUsageAggregate, readLlmUsageAggregate } from "./health/llm-usage.js";
|
|
26
28
|
import { computeDegradationMetrics, computeDenominatorFixedCoverage, computeEnrichmentMintingRollup, probeStateDbRoundTrip, } from "./health/metrics.js";
|
|
27
29
|
import { collectPluginStalenessAdvisories } from "./health/plugin-staleness.js";
|
|
@@ -30,6 +32,7 @@ import { collectSurfacesAdvisories } from "./health/surfaces.js";
|
|
|
30
32
|
import { buildPerRunSummaries } from "./health/task-runs.js";
|
|
31
33
|
import { buildTypeDirectoryAdvisory } from "./health/type-directory-check.js";
|
|
32
34
|
import { ACTIVE_RUN_WARN_MS, IMPROVE_COMPLETED_EVENT, MIN_ROWS_FOR_WORST_TASK_FAIL_RATE, } from "./health/types.js";
|
|
35
|
+
import { collectVersionDriftAdvisory } from "./health/version-drift.js";
|
|
33
36
|
import { buildWindowMetrics, computeDeltas, partitionLogBackedRows, resolveWindowCompare } from "./health/windows.js";
|
|
34
37
|
const DEFAULT_SINCE_MS = 24 * 60 * 60 * 1000;
|
|
35
38
|
export function parseHealthSince(since) {
|
|
@@ -150,23 +153,28 @@ function gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now) {
|
|
|
150
153
|
taskFailRate,
|
|
151
154
|
worstTaskFailRate: computeWorstTaskFailRate(taskRows),
|
|
152
155
|
agentFailureRate,
|
|
156
|
+
agentFailureReasonCounts: countAgentFailureReasons(agentFailures),
|
|
153
157
|
};
|
|
154
158
|
}
|
|
155
159
|
/**
|
|
156
|
-
* Config fields the surfaces advisory
|
|
157
|
-
*
|
|
158
|
-
*
|
|
160
|
+
* Config fields the surfaces advisory and (#949) the `thinking-control`
|
|
161
|
+
* check need. Best-effort: an unloadable config leaves both fields at their
|
|
162
|
+
* empty fallback and the callers degrade to their generic/unknown states.
|
|
159
163
|
*/
|
|
160
164
|
function gatherEgressConfigPhase() {
|
|
161
165
|
let egressConfigView;
|
|
166
|
+
let thinkingOffEngines = [];
|
|
162
167
|
try {
|
|
163
168
|
const config = loadConfig();
|
|
164
169
|
egressConfigView = config;
|
|
170
|
+
thinkingOffEngines = Object.entries(config.engines ?? {})
|
|
171
|
+
.filter(([, engine]) => engine.kind === "llm" && engine.enableThinking === false)
|
|
172
|
+
.map(([name]) => name);
|
|
165
173
|
}
|
|
166
174
|
catch {
|
|
167
|
-
// fall through with undefined
|
|
175
|
+
// fall through with undefined/empty
|
|
168
176
|
}
|
|
169
|
-
return { egressConfigView };
|
|
177
|
+
return { egressConfigView, thinkingOffEngines };
|
|
170
178
|
}
|
|
171
179
|
/** Extract-ledger outcome counts for the `session-extraction` check's window, independent of `--since`. */
|
|
172
180
|
function gatherSessionExtractionLedgerPhase(db, now) {
|
|
@@ -451,6 +459,7 @@ function degradedStateDbReport(hardCheck, options) {
|
|
|
451
459
|
metrics: {
|
|
452
460
|
taskFailRate: 0,
|
|
453
461
|
agentFailureRate: 0,
|
|
462
|
+
agentFailureReasonCounts: {},
|
|
454
463
|
stuckActiveRuns: 0,
|
|
455
464
|
logBackingRate: 0,
|
|
456
465
|
// `null`, not 0: the round-trip probe did not run, which is not the same
|
|
@@ -529,13 +538,30 @@ export async function akmHealth(options = {}) {
|
|
|
529
538
|
// Network probes overlap the local database phases below; awaited where consumed.
|
|
530
539
|
const engineProbesPromise = runHealthEngineProbes({ probeReachable: options.probe ? probeLlmEndpoint : undefined });
|
|
531
540
|
engineProbesPromise.catch(() => undefined);
|
|
541
|
+
// #950: same best-effort, --probe-gated discipline as engineProbesPromise
|
|
542
|
+
// above — started here, alongside it, and awaited later.
|
|
543
|
+
const versionDriftPromise = collectVersionDriftAdvisory(Boolean(options.probe), { cliVersion: pkgVersion });
|
|
544
|
+
versionDriftPromise.catch(() => undefined);
|
|
532
545
|
const taskHistory = gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now);
|
|
533
546
|
const { tableNames, missingTables, probe } = taskHistory;
|
|
534
|
-
const { egressConfigView } = gatherEgressConfigPhase();
|
|
547
|
+
const { egressConfigView, thinkingOffEngines } = gatherEgressConfigPhase();
|
|
535
548
|
const { improveSummary } = gatherImproveSummaryPhase(db, stateDbPath, since, now);
|
|
536
549
|
advisories.push(...gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, options, egressConfigView));
|
|
537
550
|
const sessionExtractionLedger = gatherSessionExtractionLedgerPhase(db, now);
|
|
551
|
+
// #950: computed once (no IO beyond config/env, same as gatherEgressConfigPhase)
|
|
552
|
+
// so `active-improve-strategy` and `engine-last-used` project the same
|
|
553
|
+
// process→engine map instead of each resolving the strategy independently.
|
|
554
|
+
const { check: activeImproveStrategy, processEngines: activeImproveStrategyEngines } = probeActiveImproveStrategy();
|
|
555
|
+
// #950: `engine-last-used` reads a fixed lookback window independent of
|
|
556
|
+
// `--since` (mirrors sessionExtractionLedger's independent window above).
|
|
557
|
+
const engineLastUsedSinceIso = engineLastUsedSince(now);
|
|
558
|
+
const engineLastUsed = readLastEngineUsage(stateDbPath, now);
|
|
559
|
+
const improveRunsInLookbackWindow = countImproveRunsSince(db, engineLastUsedSinceIso);
|
|
538
560
|
const engineProbes = await engineProbesPromise;
|
|
561
|
+
const versionDrift = await versionDriftPromise;
|
|
562
|
+
// Read once, shared by the `thinking-control` check (#949) and the
|
|
563
|
+
// `metrics.llmUsage` report field below — same window, same aggregate.
|
|
564
|
+
const llmUsage = readLlmUsageAggregate(stateDbPath, since);
|
|
539
565
|
// Run the ordered health-check registry. Each check projects the shared
|
|
540
566
|
// context computed above into one HealthCheckResult; `channel` routes it to
|
|
541
567
|
// hardChecks or advisories. Declaration order in HEALTH_CHECKS is the
|
|
@@ -554,10 +580,18 @@ export async function akmHealth(options = {}) {
|
|
|
554
580
|
stuckActiveRuns: taskHistory.stuckActiveRuns,
|
|
555
581
|
stuckActiveTasks: taskHistory.stuckActiveTasks,
|
|
556
582
|
worstTaskFailRate: taskHistory.worstTaskFailRate,
|
|
583
|
+
agentFailureReasonCounts: taskHistory.agentFailureReasonCounts,
|
|
557
584
|
sessionExtraction: improveSummary.sessionExtraction,
|
|
558
585
|
sessionExtractionLedger,
|
|
559
586
|
autoAccept: improveSummary.autoAccept,
|
|
560
587
|
engineProbes,
|
|
588
|
+
thinkingOffEngines,
|
|
589
|
+
llmUsage,
|
|
590
|
+
versionDrift,
|
|
591
|
+
activeImproveStrategy,
|
|
592
|
+
activeImproveStrategyEngines,
|
|
593
|
+
engineLastUsed,
|
|
594
|
+
improveRunsInLookbackWindow,
|
|
561
595
|
};
|
|
562
596
|
for (const check of HEALTH_CHECKS) {
|
|
563
597
|
const result = check.run(checkContext);
|
|
@@ -569,10 +603,11 @@ export async function akmHealth(options = {}) {
|
|
|
569
603
|
const metrics = {
|
|
570
604
|
taskFailRate: roundRate(taskHistory.taskFailRate),
|
|
571
605
|
agentFailureRate: roundRate(taskHistory.agentFailureRate),
|
|
606
|
+
agentFailureReasonCounts: taskHistory.agentFailureReasonCounts,
|
|
572
607
|
stuckActiveRuns: taskHistory.stuckActiveRuns,
|
|
573
608
|
logBackingRate: roundRate(taskHistory.logBackingRate),
|
|
574
609
|
probeRoundTripMs: probe.durationMs,
|
|
575
|
-
llmUsage
|
|
610
|
+
llmUsage,
|
|
576
611
|
};
|
|
577
612
|
const hardFailure = hardChecks.some((check) => check.status === "fail");
|
|
578
613
|
const deterministicWarnings = [...hardChecks, ...advisories].some((check) => check.status === "warn" && check.kind === "deterministic");
|
|
@@ -10,9 +10,11 @@ import { cacheHash } from "../content-hash.js";
|
|
|
10
10
|
/**
|
|
11
11
|
* Conservative chars-per-token estimate used when computing prompt budgets.
|
|
12
12
|
* English text averages roughly 4 chars/token for most LLM tokenizers. We use
|
|
13
|
-
* 3 to stay conservative (shorter tokens = more tokens per char).
|
|
13
|
+
* 3 to stay conservative (shorter tokens = more tokens per char). Exported so
|
|
14
|
+
* other context-length-driven budgets (e.g. reflect's direct-LLM content cap,
|
|
15
|
+
* #952) share this single estimate instead of redeclaring it.
|
|
14
16
|
*/
|
|
15
|
-
const CHARS_PER_TOKEN = 3;
|
|
17
|
+
export const CHARS_PER_TOKEN = 3;
|
|
16
18
|
/**
|
|
17
19
|
* Overhead budget reserved for the system prompt, chunk header lines, and per-
|
|
18
20
|
* memory metadata lines (name, description, tags, separator). Measured at
|
|
@@ -8,7 +8,7 @@ import { getStringArg, parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
|
8
8
|
import { GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
9
9
|
import { isFullRefInput, parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
10
10
|
import { loadConfig } from "../../core/config/config.js";
|
|
11
|
-
import { UsageError } from "../../core/errors.js";
|
|
11
|
+
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
12
12
|
import { resolveMutationTarget } from "../../core/mutation-target.js";
|
|
13
13
|
import { getCacheDir } from "../../core/paths.js";
|
|
14
14
|
import { redactSensitiveText } from "../../core/redaction.js";
|
|
@@ -16,9 +16,11 @@ import { clearLogFile, setLogFile, warn } from "../../core/warn.js";
|
|
|
16
16
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
17
17
|
import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
|
|
18
18
|
import { akmImprove } from "./improve.js";
|
|
19
|
+
import { runImproveReportQuery } from "./improve-report.js";
|
|
19
20
|
import { buildImproveRunId, recordImproveRunResult, recordTerminatedImproveRun, } from "./improve-result-file.js";
|
|
20
21
|
import { runImproveSession } from "./improve-session.js";
|
|
21
22
|
import { resolveImprovePlan } from "./improve-strategies.js";
|
|
23
|
+
import { formatUsageReportTable } from "./improve-usage-report.js";
|
|
22
24
|
let akmImproveForRun = akmImprove;
|
|
23
25
|
/** Swap the CLI's improve work implementation in deterministic subprocess tests. */
|
|
24
26
|
export function _setAkmImproveForTests(fake) {
|
|
@@ -74,6 +76,57 @@ function rejectRetiredImproveTargetFlag() {
|
|
|
74
76
|
return;
|
|
75
77
|
throw new UsageError("`akm improve --target` was renamed to `--bundle` in 0.9. Use `--bundle <name>` instead.", "INVALID_FLAG_VALUE");
|
|
76
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* `--require-engines` (#957): abort before any lock, log, or index side
|
|
81
|
+
* effect when the resolved plan already knows a process the active strategy
|
|
82
|
+
* would enable cannot run. Without this flag improve degrades gracefully —
|
|
83
|
+
* it skips the affected processes and reports them in `skippedProcesses` —
|
|
84
|
+
* which is right for an interactive run but wrong for a scheduled one that
|
|
85
|
+
* would rather fail loudly than burn its budget re-indexing and then skip
|
|
86
|
+
* everything. Names the unresolved credential reference per process (not
|
|
87
|
+
* just the process name) so an operator whose own shell passes config
|
|
88
|
+
* validation can see exactly what the scheduler's environment is missing.
|
|
89
|
+
*/
|
|
90
|
+
function assertRequiredEnginesAvailable(plan) {
|
|
91
|
+
if (plan.engineUnavailable.length === 0)
|
|
92
|
+
return;
|
|
93
|
+
const lines = plan.engineUnavailable.map((item) => ` - ${item.process} (${item.configKey}): ${item.reason}`);
|
|
94
|
+
throw new ConfigError(`--require-engines: ${plan.engineUnavailable.length} improve process${plan.engineUnavailable.length === 1 ? "" : "es"} cannot run because ${plan.engineUnavailable.length === 1 ? "its" : "their"} engine is unavailable:\n${lines.join("\n")}`, "LLM_NOT_CONFIGURED");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `akm improve report` (#944): a scope value that dispatches to the per-run
|
|
98
|
+
* LLM usage/routing report instead of a real improve run — "report" is not,
|
|
99
|
+
* and will never be, a real asset type (`DEFAULT_ALLOWED_TYPES` in
|
|
100
|
+
* improve-strategies.ts), so this already matched zero assets before this
|
|
101
|
+
* flag existed, matching the precedent `rejectRetiredCanaryScope` set for
|
|
102
|
+
* intercepting a special scope word ahead of any lock/log/index side effect.
|
|
103
|
+
*/
|
|
104
|
+
function runImproveReportCli(args) {
|
|
105
|
+
const runIdArg = getStringArg(args, "run");
|
|
106
|
+
const sinceArg = getStringArg(args, "since");
|
|
107
|
+
const result = runImproveReportQuery({ runId: runIdArg, since: sinceArg });
|
|
108
|
+
output("improve-report", { ok: true, ...result });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* `--run`/`--since` only mean anything with the "report" scope, which
|
|
112
|
+
* intercepts before this point in the `run` handler below. citty is
|
|
113
|
+
* non-strict, so passing either with a real scope (or no scope at all) used
|
|
114
|
+
* to be silently ignored — the flag's value was read nowhere else, and the
|
|
115
|
+
* run proceeded as an ordinary improve run with no error, discarding the
|
|
116
|
+
* operator's intent. Reject explicitly instead, matching the precedent
|
|
117
|
+
* `rejectRetiredCanaryScope`/`rejectRetiredImproveTargetFlag` set for other
|
|
118
|
+
* flag misuse on this command.
|
|
119
|
+
*/
|
|
120
|
+
function rejectReportOnlyFlags(args) {
|
|
121
|
+
const flag = getStringArg(args, "run") !== undefined
|
|
122
|
+
? "--run"
|
|
123
|
+
: getStringArg(args, "since") !== undefined
|
|
124
|
+
? "--since"
|
|
125
|
+
: undefined;
|
|
126
|
+
if (flag === undefined)
|
|
127
|
+
return;
|
|
128
|
+
throw new UsageError(`\`${flag}\` only applies to \`akm improve report\`. Use \`akm improve report ${flag} <value>\` instead.`, "INVALID_FLAG_VALUE");
|
|
129
|
+
}
|
|
77
130
|
export const improveCommand = defineCommand({
|
|
78
131
|
meta: {
|
|
79
132
|
name: "improve",
|
|
@@ -91,6 +144,11 @@ export const improveCommand = defineCommand({
|
|
|
91
144
|
},
|
|
92
145
|
task: { type: "string", description: "Add extra guidance for this improvement pass" },
|
|
93
146
|
"dry-run": { type: "boolean", description: "Show planned actions without writing", default: false },
|
|
147
|
+
plan: {
|
|
148
|
+
type: "boolean",
|
|
149
|
+
description: "Alias for --dry-run (#947). Sets the exact same internal flag; use it when previewing resolved process -> engine -> model routing (plan.processes) rather than checking what would write.",
|
|
150
|
+
default: false,
|
|
151
|
+
},
|
|
94
152
|
bundle: { type: "string", description: "Override the write target for accepted proposals" },
|
|
95
153
|
limit: { type: "string", description: "Maximum number of assets to process (highest utility first)" },
|
|
96
154
|
"timeout-ms": {
|
|
@@ -112,6 +170,19 @@ export const improveCommand = defineCommand({
|
|
|
112
170
|
description: "If another improve run already holds the lock, skip gracefully (exit 0) instead of failing with 'already running' (exit 78). Use for high-frequency scheduled runs so they don't pile up failures while a longer run is in progress.",
|
|
113
171
|
default: false,
|
|
114
172
|
},
|
|
173
|
+
"require-engines": {
|
|
174
|
+
type: "boolean",
|
|
175
|
+
description: "Abort before any indexing, lock, or log side effect (exit 78) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment. Without this flag, improve degrades gracefully instead: it skips the affected processes and reports them in the result's skippedProcesses. Recommended alongside --skip-if-locked for scheduled runs.",
|
|
176
|
+
default: false,
|
|
177
|
+
},
|
|
178
|
+
run: {
|
|
179
|
+
type: "string",
|
|
180
|
+
description: 'Only with the "report" scope (`akm improve report --run <id>`): show the LLM usage/routing report for one specific improve_runs row instead of the most recent run. Mutually exclusive with --since.',
|
|
181
|
+
},
|
|
182
|
+
since: {
|
|
183
|
+
type: "string",
|
|
184
|
+
description: 'Only with the "report" scope (`akm improve report --since <window>`): aggregate the LLM usage/routing report over every real run started since <window> (a duration like "24h"/"7d", or an ISO timestamp) instead of showing one run. Mutually exclusive with --run.',
|
|
185
|
+
},
|
|
115
186
|
strategy: {
|
|
116
187
|
type: "string",
|
|
117
188
|
description: "Named improve strategy from improve.strategies or built-in strategies (catchup, consolidate, default, graph-refresh, proactive-maintenance, quick, reflect-distill, thorough). Controls which sub-processes run and which asset types are processed.",
|
|
@@ -127,6 +198,13 @@ export const improveCommand = defineCommand({
|
|
|
127
198
|
},
|
|
128
199
|
async run({ args }) {
|
|
129
200
|
await runWithJsonErrors(async () => {
|
|
201
|
+
// #944 — dispatch before any lock/log/index side effect, same
|
|
202
|
+
// interception point as rejectRetiredCanaryScope below.
|
|
203
|
+
if (getStringArg(args, "scope") === "report") {
|
|
204
|
+
runImproveReportCli(args);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
rejectReportOnlyFlags(args);
|
|
130
208
|
rejectRetiredImproveTargetFlag();
|
|
131
209
|
// D7 — `--format` used to be rejected here outright. It is a global flag on
|
|
132
210
|
// a command that does emit an envelope through `output()` (always on
|
|
@@ -137,7 +215,9 @@ export const improveCommand = defineCommand({
|
|
|
137
215
|
const jsonToStdout = args["json-to-stdout"];
|
|
138
216
|
const targetArg = getStringArg(args, "bundle");
|
|
139
217
|
const taskArg = getStringArg(args, "task");
|
|
140
|
-
|
|
218
|
+
// #947 — `--plan` is a zero-logic discoverability alias for `--dry-run`;
|
|
219
|
+
// it must never fork the computation, only set the same flag.
|
|
220
|
+
const dryRun = args["dry-run"] || args.plan;
|
|
141
221
|
const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
|
|
142
222
|
const timeoutMs = parsePositiveIntFlag(args["timeout-ms"], "--timeout-ms");
|
|
143
223
|
const requireFeedbackSignal = args["require-feedback-signal"];
|
|
@@ -154,7 +234,13 @@ export const improveCommand = defineCommand({
|
|
|
154
234
|
: resolveWriteTarget(effectiveConfig, targetArg);
|
|
155
235
|
// Resolve every enabled model-backed process before logging, signal
|
|
156
236
|
// lifecycle setup, or any filesystem/database side effect.
|
|
157
|
-
|
|
237
|
+
// #800/#957 round 3 — `--dry-run`/`--plan` never dispatches, so the
|
|
238
|
+
// "no improve process can run" guard must not throw when every process
|
|
239
|
+
// is disabled purely by an unreachable credential; a live run keeps
|
|
240
|
+
// throwing (allowAllDisabled unset).
|
|
241
|
+
const resolvedPlan = resolveImprovePlan(strategyArg, effectiveConfig, { allowAllDisabled: Boolean(dryRun) });
|
|
242
|
+
if (args["require-engines"])
|
|
243
|
+
assertRequiredEnginesAvailable(resolvedPlan);
|
|
158
244
|
const selectedStrategyName = resolvedPlan.strategy.name;
|
|
159
245
|
const sensitiveValues = collectEngineCredentialValues(effectiveConfig);
|
|
160
246
|
// Only set the keys the user actually passed (citty leaves the flag
|
|
@@ -271,8 +357,9 @@ export const improveCommand = defineCommand({
|
|
|
271
357
|
// `improve_runs` table of state.db (migration 003) and emit NOTHING
|
|
272
358
|
// on stdout. The verbose JSON would otherwise scroll earlier progress
|
|
273
359
|
// logs out of the terminal buffer. The existing `[improve] ...`
|
|
274
|
-
// progress log lines on stderr remain the canonical console UX —
|
|
275
|
-
//
|
|
360
|
+
// progress log lines on stderr remain the canonical console UX — the
|
|
361
|
+
// usage-report table below (#944) follows that same convention
|
|
362
|
+
// (stderr, `[improve]`-prefixed), it is not new stdout noise.
|
|
276
363
|
//
|
|
277
364
|
// Pre-0.8.0 wrote `<stash>/.akm/runs/<run-id>/improve-result.json`;
|
|
278
365
|
// those files are no longer authored. Query recent runs with:
|
|
@@ -295,6 +382,13 @@ export const improveCommand = defineCommand({
|
|
|
295
382
|
else {
|
|
296
383
|
process.stderr.write(`warning: no writable bundle directory resolved; improve result not persisted to state.db (use --json-to-stdout to capture)\n`);
|
|
297
384
|
}
|
|
385
|
+
// #944 — same table `akm improve report` renders, appended to every
|
|
386
|
+
// real (non-dry-run) run so an operator sees the routing/cost split
|
|
387
|
+
// without a separate command. Omitted when the run made no LLM calls
|
|
388
|
+
// and skipped no enabled process (nothing to report).
|
|
389
|
+
if (improveResult.usageReport) {
|
|
390
|
+
process.stderr.write(`${formatUsageReportTable(improveResult.usageReport)}\n`);
|
|
391
|
+
}
|
|
298
392
|
if (jsonToStdout)
|
|
299
393
|
output("improve", improveResult);
|
|
300
394
|
// F4: was `process.exit(0)` — the run has already been fully recorded
|