akm-cli 0.9.0-rc.3 → 0.9.0-rc.4
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 +6 -4
- package/README.md +7 -8
- package/SECURITY.md +5 -3
- package/dist/akm +44 -33
- package/dist/akm-migrate-storage +44 -33
- package/dist/assets/backends/schtasks-template.xml +2 -1
- package/dist/cli.js +25 -10
- package/dist/commands/health/html-report.js +23 -2
- package/dist/commands/health/improve-metrics.js +45 -6
- package/dist/commands/health/md-report.js +10 -1
- package/dist/commands/health/windows.js +1 -1
- package/dist/commands/health.js +1 -1
- package/dist/commands/tasks/default-tasks.js +5 -5
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +261 -68
- package/dist/core/improve-result.js +87 -7
- package/dist/core/state-db.js +1 -0
- package/dist/scripts/migrate-storage.js +21 -3
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +89 -8
- package/dist/storage/repositories/task-history-repository.js +30 -3
- package/dist/tasks/backends/cron.js +71 -38
- package/dist/tasks/backends/exec-utils.js +55 -3
- package/dist/tasks/backends/launchd.js +241 -50
- package/dist/tasks/backends/schtasks.js +434 -59
- package/dist/tasks/command-executable.js +93 -0
- package/dist/tasks/parser.js +142 -6
- package/dist/tasks/resolve-akm-bin.js +19 -4
- package/dist/tasks/runner.js +258 -93
- package/dist/tasks/schedule.js +108 -19
- package/dist/tasks/scheduler-invocation.js +86 -0
- package/dist/tasks/task-id.js +37 -0
- package/docs/README.md +103 -0
- package/docs/migration/release-notes/0.9.0.md +4 -2
- package/docs/migration/v0.8-to-v0.9.md +64 -9
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -1234,14 +1234,16 @@ proposal and log storage, `--format html` output, and per-stage LLM telemetry.
|
|
|
1234
1234
|
|
|
1235
1235
|
### Added
|
|
1236
1236
|
|
|
1237
|
-
- **Cross-runtime: akm now runs on Node.js
|
|
1237
|
+
- **Cross-runtime: akm now runs on Node.js >= 20.12 in addition to Bun** (#560,
|
|
1238
1238
|
#465). A two-file runtime boundary (`src/storage/database.ts` owns SQLite via
|
|
1239
1239
|
`bun:sqlite` on Bun / `better-sqlite3` on Node; `src/runtime.ts` owns every
|
|
1240
1240
|
`Bun.*` API) contains all runtime-specific code, enforced by a lint guard so it
|
|
1241
1241
|
cannot leak back out. A CI `node-smoke` matrix runs the built CLI under Node
|
|
1242
|
-
20 and 22.
|
|
1243
|
-
|
|
1244
|
-
Bun
|
|
1242
|
+
20 and 22. The prompts dependency (`@clack/core`) uses `node:util.styleText`,
|
|
1243
|
+
added in Node 20.12; Node 18 is EOL and unsupported. The npm package uses Node
|
|
1244
|
+
as its bootstrap and prefers a working Bun >= 1.0 for execution when both are
|
|
1245
|
+
available. Old, unusable, or absent Bun installations fall back to Node.js;
|
|
1246
|
+
standalone binaries remain runtime-free.
|
|
1245
1247
|
- **`session` asset type — agent sessions are now searchable** (#561). The
|
|
1246
1248
|
`extract` pass, after distilling memory proposals from a session, additionally
|
|
1247
1249
|
writes the session itself as a first-class `session` asset
|
package/README.md
CHANGED
|
@@ -24,13 +24,7 @@ curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh |
|
|
|
24
24
|
irm https://github.com/itlackey/akm/releases/latest/download/install.ps1 | iex
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
**Option 2 —
|
|
28
|
-
|
|
29
|
-
```sh
|
|
30
|
-
bun install -g akm-cli
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
**Option 3 — Node.js (requires Node.js >= 20.12):**
|
|
27
|
+
**Option 2 — npm package (requires [Node.js](https://nodejs.org) >= 20.12):**
|
|
34
28
|
|
|
35
29
|
```sh
|
|
36
30
|
npm install -g akm-cli
|
|
@@ -38,7 +32,12 @@ npm install -g akm-cli
|
|
|
38
32
|
|
|
39
33
|
Upgrade in place with `akm upgrade`.
|
|
40
34
|
|
|
41
|
-
|
|
35
|
+
The npm package always uses Node.js to bootstrap its cross-platform command.
|
|
36
|
+
If a working [Bun](https://bun.sh) >= 1.0 is also on `PATH`, the launcher
|
|
37
|
+
prefers Bun for execution; old, unusable, or absent Bun installations fall back
|
|
38
|
+
to Node.js. Node.js remains required for the npm package. The standalone
|
|
39
|
+
binaries are runtime-free.
|
|
40
|
+
|
|
42
41
|
> The old `vault` asset type was removed in 0.9.0; use `env` for whole `.env`
|
|
43
42
|
> groups and `secret` for standalone sensitive values.
|
|
44
43
|
|
package/SECURITY.md
CHANGED
|
@@ -82,9 +82,11 @@ containing secrets or private notes.
|
|
|
82
82
|
|
|
83
83
|
## Known non-issues
|
|
84
84
|
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
- **The `akm-cli` npm package requires Node.js >= 20.12 as its bootstrap.** A
|
|
86
|
+
working Bun >= 1.0 is preferred for execution when it is also on `PATH`; old,
|
|
87
|
+
unusable, or absent Bun installations fall back to Node.js. Bun does not
|
|
88
|
+
remove the package's Node.js requirement. Standalone binaries are
|
|
89
|
+
runtime-free. This is a compatibility limitation, not a security risk.
|
|
88
90
|
- **Workflows can read any file the akm process can read.** This is not a
|
|
89
91
|
bug — see "Threat model" above.
|
|
90
92
|
- **Installing `akm-cli` runs the preinstall hook.** The hook only validates
|
package/dist/akm
CHANGED
|
@@ -1,38 +1,49 @@
|
|
|
1
|
-
#!/bin/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
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
5
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
16
8
|
|
|
17
|
-
if
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
done
|
|
25
|
-
fi
|
|
9
|
+
if (!process.versions.bun) {
|
|
10
|
+
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
11
|
+
if (major < 20 || (major === 20 && minor < 12)) {
|
|
12
|
+
console.error("The akm-cli npm package requires Node.js >= 20.12 to bootstrap.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
26
16
|
|
|
27
|
-
|
|
17
|
+
const bunProbe = process.versions.bun
|
|
18
|
+
? { status: 0, stdout: process.versions.bun }
|
|
19
|
+
: spawnSync("bun", ["--version"], {
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
22
|
+
timeout: 5_000,
|
|
23
|
+
});
|
|
24
|
+
const [bunMajor = 0] = (bunProbe.status === 0 ? bunProbe.stdout.trim() : "").split(".").map(Number);
|
|
25
|
+
const useBun = bunMajor >= 1;
|
|
26
|
+
const bunEntry = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
27
|
+
const nodeEntry = fileURLToPath(new URL("./cli-node.mjs", import.meta.url));
|
|
28
28
|
|
|
29
|
-
if
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
if (!useBun && !process.versions.bun) {
|
|
30
|
+
await import("./cli-node.mjs");
|
|
31
|
+
} else {
|
|
32
|
+
const command = useBun ? (process.versions.bun ? process.execPath : "bun") : "node";
|
|
33
|
+
const entry = useBun ? bunEntry : nodeEntry;
|
|
34
|
+
const runtime = useBun ? "Bun" : "Node.js";
|
|
35
|
+
const result = await new Promise((resolve) => {
|
|
36
|
+
const child = spawn(command, [entry, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
37
|
+
child.once("error", (error) => resolve({ error }));
|
|
38
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
39
|
+
});
|
|
32
40
|
|
|
33
|
-
if
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
if (result.error) {
|
|
42
|
+
console.error(`akm launcher could not start ${runtime}: ${result.error.message}`);
|
|
43
|
+
process.exitCode = 127;
|
|
44
|
+
} else if (result.signal) {
|
|
45
|
+
process.kill(process.pid, result.signal);
|
|
46
|
+
} else {
|
|
47
|
+
process.exitCode = result.code ?? 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/akm-migrate-storage
CHANGED
|
@@ -1,38 +1,49 @@
|
|
|
1
|
-
#!/bin/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
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
5
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
16
8
|
|
|
17
|
-
if
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
done
|
|
25
|
-
fi
|
|
9
|
+
if (!process.versions.bun) {
|
|
10
|
+
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
11
|
+
if (major < 20 || (major === 20 && minor < 12)) {
|
|
12
|
+
console.error("The akm-cli npm package requires Node.js >= 20.12 to bootstrap.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
26
16
|
|
|
27
|
-
|
|
17
|
+
const bunProbe = process.versions.bun
|
|
18
|
+
? { status: 0, stdout: process.versions.bun }
|
|
19
|
+
: spawnSync("bun", ["--version"], {
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
22
|
+
timeout: 5_000,
|
|
23
|
+
});
|
|
24
|
+
const [bunMajor = 0] = (bunProbe.status === 0 ? bunProbe.stdout.trim() : "").split(".").map(Number);
|
|
25
|
+
const useBun = bunMajor >= 1;
|
|
26
|
+
const bunEntry = fileURLToPath(new URL("./scripts/migrate-storage.js", import.meta.url));
|
|
27
|
+
const nodeEntry = fileURLToPath(new URL("./migrate-storage-node.mjs", import.meta.url));
|
|
28
28
|
|
|
29
|
-
if
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
if (!useBun && !process.versions.bun) {
|
|
30
|
+
await import("./migrate-storage-node.mjs");
|
|
31
|
+
} else {
|
|
32
|
+
const command = useBun ? (process.versions.bun ? process.execPath : "bun") : "node";
|
|
33
|
+
const entry = useBun ? bunEntry : nodeEntry;
|
|
34
|
+
const runtime = useBun ? "Bun" : "Node.js";
|
|
35
|
+
const result = await new Promise((resolve) => {
|
|
36
|
+
const child = spawn(command, [entry, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
37
|
+
child.once("error", (error) => resolve({ error }));
|
|
38
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
39
|
+
});
|
|
32
40
|
|
|
33
|
-
if
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
if (result.error) {
|
|
42
|
+
console.error(`akm-migrate-storage launcher could not start ${runtime}: ${result.error.message}`);
|
|
43
|
+
process.exitCode = 127;
|
|
44
|
+
} else if (result.signal) {
|
|
45
|
+
process.kill(process.pid, result.signal);
|
|
46
|
+
} else {
|
|
47
|
+
process.exitCode = result.code ?? 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
<RegistrationInfo>
|
|
4
4
|
<Description>akm scheduled task: {{TASK_ID}}</Description>
|
|
5
5
|
<URI>{{FOLDER}}{{TASK_ID}}</URI>
|
|
6
|
+
<Source>{{SIGNATURE}}</Source>
|
|
7
|
+
<Documentation>Log target (informational only; schtasks doesn't redirect): {{LOG_PATH}}</Documentation>
|
|
6
8
|
</RegistrationInfo>
|
|
7
9
|
<Triggers>
|
|
8
10
|
{{TRIGGER_XML}}
|
|
@@ -25,5 +27,4 @@
|
|
|
25
27
|
<Arguments>{{ARGS}}</Arguments>
|
|
26
28
|
</Exec>
|
|
27
29
|
</Actions>
|
|
28
|
-
<!-- Log target (informational only; schtasks doesn't redirect): {{LOG_PATH}} -->
|
|
29
30
|
</Task>
|
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
3
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
4
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
-
// Runtime guard: akm-cli
|
|
6
|
-
// #560)
|
|
7
|
-
//
|
|
5
|
+
// Runtime guard: the akm-cli npm package bootstraps with Node.js >= 20.12
|
|
6
|
+
// (#465, #560), then its launcher prefers a working Bun >= 1.0 when available.
|
|
7
|
+
// The runtime boundary (src/runtime.ts, src/storage/database.ts) supports both.
|
|
8
|
+
// Under Node the CLI must be launched via the
|
|
8
9
|
// `dist/cli-node.mjs` wrapper, which registers the text-import loader hook
|
|
9
10
|
// before this module graph loads; running `node dist/cli.js` directly still
|
|
10
11
|
// works for code paths that touch no embedded text asset, but the wrapper is
|
|
@@ -18,12 +19,11 @@
|
|
|
18
19
|
.map((part) => Number.parseInt(part, 10) || 0);
|
|
19
20
|
const nodeOk = major > 20 || (major === 20 && (minor > 12 || (minor === 12 && patch >= 0)));
|
|
20
21
|
if (!nodeOk) {
|
|
21
|
-
console.error("\n ERROR: akm-cli
|
|
22
|
+
console.error("\n ERROR: the akm-cli npm package requires Node.js >= 20.12.\n" +
|
|
22
23
|
` Detected Node.js ${process.versions.node ?? "unknown"}.\n` +
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
" 3. Binary: curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\n");
|
|
24
|
+
" Bun >= 1.0 is optional for execution; it does not replace the Node.js bootstrap.\n" +
|
|
25
|
+
" Upgrade Node.js (https://nodejs.org), or install the runtime-free standalone binary:\n" +
|
|
26
|
+
" curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\n");
|
|
27
27
|
process.exit(1);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -60,7 +60,7 @@ process.on("uncaughtException", (err) => {
|
|
|
60
60
|
import fs from "node:fs";
|
|
61
61
|
import path from "node:path";
|
|
62
62
|
import { defineCommand, runMain } from "citty";
|
|
63
|
-
import { findCittyTopLevelCommand } from "./cli/parse-args.js";
|
|
63
|
+
import { findCittyTopLevelCommand, findCittyTopLevelCommandIndex } from "./cli/parse-args.js";
|
|
64
64
|
import { EXIT_CODES, emitJsonError, output, parseAllFlagValues, runWithJsonErrors } from "./cli/shared.js";
|
|
65
65
|
import { agentCommand, lintCommand, proposeCommand } from "./commands/agent/contribute-cli.js";
|
|
66
66
|
import { backupCommand } from "./commands/backup-cli.js";
|
|
@@ -93,6 +93,7 @@ import { workflowCommand } from "./commands/workflow-cli.js";
|
|
|
93
93
|
import { bestEffort } from "./core/best-effort.js";
|
|
94
94
|
import { DEFAULT_CONFIG, loadConfig } from "./core/config/config.js";
|
|
95
95
|
import { UsageError } from "./core/errors.js";
|
|
96
|
+
import { assertNoPendingMigrationOperation } from "./core/migration-operation.js";
|
|
96
97
|
import { getCacheDir, getConfigPath, getDbPath } from "./core/paths.js";
|
|
97
98
|
import { plainize } from "./core/tty.js";
|
|
98
99
|
import { info, isQuiet, setQuiet, setVerbose, warn } from "./core/warn.js";
|
|
@@ -541,14 +542,26 @@ export const main = defineCommand({
|
|
|
541
542
|
},
|
|
542
543
|
});
|
|
543
544
|
const MAIN_TOP_LEVEL_ARGS = main.args;
|
|
545
|
+
function isTaskRunWithId(argv) {
|
|
546
|
+
const args = argv.slice(2);
|
|
547
|
+
const commandIndex = findCittyTopLevelCommandIndex(args, MAIN_TOP_LEVEL_ARGS);
|
|
548
|
+
const command = commandIndex >= 0 ? args[commandIndex] : undefined;
|
|
549
|
+
if (command !== "tasks" && command !== "task")
|
|
550
|
+
return false;
|
|
551
|
+
const taskArgs = args.slice(commandIndex + 1);
|
|
552
|
+
return taskArgs[0] === "run" && typeof taskArgs[1] === "string" && !taskArgs[1].startsWith("-");
|
|
553
|
+
}
|
|
544
554
|
/** Recovery/setup surfaces must remain reachable when config.json is invalid. */
|
|
545
555
|
export function shouldBypassConfigStartup(argv) {
|
|
546
556
|
const args = argv.slice(2);
|
|
547
557
|
if (args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-v"))
|
|
548
558
|
return true;
|
|
549
|
-
const
|
|
559
|
+
const commandIndex = findCittyTopLevelCommandIndex(args, MAIN_TOP_LEVEL_ARGS);
|
|
560
|
+
const command = commandIndex >= 0 ? args[commandIndex] : undefined;
|
|
550
561
|
if (command === "setup" || command === "backup" || command === "migrate")
|
|
551
562
|
return true;
|
|
563
|
+
if (isTaskRunWithId(argv))
|
|
564
|
+
return true;
|
|
552
565
|
if (command !== "config")
|
|
553
566
|
return false;
|
|
554
567
|
const configIndex = args.indexOf("config");
|
|
@@ -584,6 +597,8 @@ if (import.meta.main || process.env.AKM_NODE_ENTRY === "1") {
|
|
|
584
597
|
// rather than letting the raw exception escape with a stack trace.
|
|
585
598
|
try {
|
|
586
599
|
applyEarlyStderrFlags(process.argv);
|
|
600
|
+
if (isTaskRunWithId(process.argv))
|
|
601
|
+
assertNoPendingMigrationOperation();
|
|
587
602
|
const bypassConfig = shouldBypassConfigStartup(process.argv);
|
|
588
603
|
initOutputMode(process.argv, bypassConfig ? (DEFAULT_CONFIG.output ?? {}) : (loadConfig().output ?? {}));
|
|
589
604
|
}
|
|
@@ -92,6 +92,8 @@ function reshapeRun(r) {
|
|
|
92
92
|
const geMs = ge.durationMs || 0;
|
|
93
93
|
return {
|
|
94
94
|
id: r.id,
|
|
95
|
+
resultStatus: r.resultStatus ?? "valid",
|
|
96
|
+
resultComplete: r.resultComplete ?? (r.resultStatus === undefined || r.resultStatus === "valid"),
|
|
95
97
|
taskId: r.taskId ?? "manual",
|
|
96
98
|
strategy: r.strategy,
|
|
97
99
|
legacyProfile: r.legacyProfile,
|
|
@@ -262,7 +264,10 @@ function buildEchartsTag(opts) {
|
|
|
262
264
|
*/
|
|
263
265
|
export function buildHealthHtmlReplacements(result, opts) {
|
|
264
266
|
const deltas = opts.deltas ?? {};
|
|
265
|
-
const
|
|
267
|
+
const compareRuns = (a, b) => a.startedAt.localeCompare(b.startedAt) || a.completedAt.localeCompare(b.completedAt) || a.id.localeCompare(b.id);
|
|
268
|
+
const allRuns = (result.runs ?? []).map(reshapeRun).sort(compareRuns);
|
|
269
|
+
const invalidRuns = allRuns.filter((run) => run.resultStatus === "invalid");
|
|
270
|
+
const runs = allRuns.filter((run) => run.resultStatus !== "invalid");
|
|
266
271
|
const improve = result.improve;
|
|
267
272
|
const proposals = opts.proposals;
|
|
268
273
|
const sem = readSemSearch(result.advisories);
|
|
@@ -320,7 +325,10 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
320
325
|
const reportTitle = reportDate ? `AKM Health Report — ${reportDate}` : "AKM Health Report";
|
|
321
326
|
const lastRun = runs[runs.length - 1];
|
|
322
327
|
const generatedAt = lastRun ? lastRun.completedAt || lastRun.startedAt || sinceIso : sinceIso;
|
|
323
|
-
const latest = [...runs]
|
|
328
|
+
const latest = [...runs]
|
|
329
|
+
.filter((run) => run.resultComplete)
|
|
330
|
+
.sort(compareRuns)
|
|
331
|
+
.at(-1);
|
|
324
332
|
// Freshness: surface the newest run's timestamp + the generated-at anchor in
|
|
325
333
|
// the exec card. Staleness is computed deterministically (no Date.now()) from
|
|
326
334
|
// the gap between the window start (`since`) and the newest run we have: if no
|
|
@@ -389,6 +397,9 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
389
397
|
li("Report window", esc(opts.window)),
|
|
390
398
|
li("Compare window", esc(opts.compare)),
|
|
391
399
|
li("Runs", `${num(totalRuns)} (${failedRuns} failed)`),
|
|
400
|
+
li("Included result rows", num(improve.resultRows?.included ?? totalRuns)),
|
|
401
|
+
li("Normalized result rows", num(improve.resultRows?.normalized ?? 0)),
|
|
402
|
+
li("Invalid result rows skipped", num(improve.resultRows?.skipped.invalid ?? invalidRuns.length)),
|
|
392
403
|
li("Stash derived", `<abbr title="Whole-stash recount of derived assets at report time — not a per-run sum.">${num(improve.memorySummary.derived)}</abbr>`),
|
|
393
404
|
li("Stash eligible", `<abbr title="Whole-stash recount of eligible assets at report time — not a per-run sum.">${num(improve.memorySummary.eligible)}</abbr>`),
|
|
394
405
|
li("Pending proposals", String(proposals.length)),
|
|
@@ -655,6 +666,16 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
655
666
|
seen.add(item.key);
|
|
656
667
|
items.push(item);
|
|
657
668
|
};
|
|
669
|
+
if (invalidRuns.length > 0) {
|
|
670
|
+
pushItem({
|
|
671
|
+
key: "invalid-improve-result-rows",
|
|
672
|
+
prio: "P2",
|
|
673
|
+
cls: "warn",
|
|
674
|
+
title: `${invalidRuns.length} invalid improve result row${invalidRuns.length === 1 ? "" : "s"} skipped`,
|
|
675
|
+
descHtml: `Excluded from result-derived metrics: ${invalidRuns.map((run) => `<code>${esc(run.id)}</code>`).join(", ")}.`,
|
|
676
|
+
remedy: `akm health --since=${opts.window} --group-by run`,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
658
679
|
// Hard advisories from the health check engine (own remediation in message).
|
|
659
680
|
for (const a of result.advisories) {
|
|
660
681
|
if (a.status !== "warn" && a.status !== "fail")
|
|
@@ -25,6 +25,7 @@ function createUnknownImproveMetrics() {
|
|
|
25
25
|
completed: 0,
|
|
26
26
|
skipped: 0,
|
|
27
27
|
skipReasons: {},
|
|
28
|
+
resultRows: { total: 0, included: 0, normalized: 0, skipped: { invalid: 0 } },
|
|
28
29
|
plannedRefs: 0,
|
|
29
30
|
strategyFilteredRefs: 0,
|
|
30
31
|
actions: {
|
|
@@ -605,6 +606,15 @@ function mergeImproveMetrics(dst, src) {
|
|
|
605
606
|
// totalAssets is intentionally NOT merged here — set from the most recent run in summarizeImproveRuns.
|
|
606
607
|
dst.coverage.acceptedProposals += src.coverage.acceptedProposals;
|
|
607
608
|
}
|
|
609
|
+
function compareImproveRunRecency(a, b) {
|
|
610
|
+
const started = a.started_at.localeCompare(b.started_at);
|
|
611
|
+
if (started !== 0)
|
|
612
|
+
return started;
|
|
613
|
+
const completed = (a.completed_at ?? "").localeCompare(b.completed_at ?? "");
|
|
614
|
+
if (completed !== 0)
|
|
615
|
+
return completed;
|
|
616
|
+
return a.id.localeCompare(b.id);
|
|
617
|
+
}
|
|
608
618
|
export function summarizeImproveRuns(db, since, until) {
|
|
609
619
|
const accum = createUnknownImproveMetrics();
|
|
610
620
|
const rows = queryImproveRuns(db, since, until);
|
|
@@ -618,16 +628,33 @@ export function summarizeImproveRuns(db, since, until) {
|
|
|
618
628
|
};
|
|
619
629
|
// memorySummary is a whole-stash snapshot per run, so the window value is the
|
|
620
630
|
// MOST RECENT run's snapshot (current state) — not a sum across runs.
|
|
621
|
-
let
|
|
631
|
+
let latestCompleteRow;
|
|
622
632
|
let latestMemorySummary;
|
|
623
633
|
let latestStrategyFilteredRefs = 0;
|
|
634
|
+
if (!accum.resultRows)
|
|
635
|
+
throw new Error("invariant: improve result-row accounting was not initialized");
|
|
636
|
+
accum.resultRows.total = rows.length;
|
|
624
637
|
for (const row of rows) {
|
|
625
|
-
|
|
638
|
+
let decoded;
|
|
639
|
+
try {
|
|
640
|
+
decoded = decodeImproveResult(row.result_json);
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
accum.resultRows.skipped.invalid += 1;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
accum.resultRows.included += 1;
|
|
647
|
+
if (decoded.normalizedLegacyPartial)
|
|
648
|
+
accum.resultRows.normalized += 1;
|
|
649
|
+
const result = decoded.envelope;
|
|
626
650
|
const perRow = projectRunMetrics(result);
|
|
627
651
|
mergeImproveMetrics(accum, perRow);
|
|
628
652
|
const startMs = new Date(row.started_at).getTime();
|
|
629
|
-
if (
|
|
630
|
-
|
|
653
|
+
if (!decoded.normalizedLegacyPartial &&
|
|
654
|
+
result.terminated === undefined &&
|
|
655
|
+
Number.isFinite(startMs) &&
|
|
656
|
+
(latestCompleteRow === undefined || compareImproveRunRecency(row, latestCompleteRow) > 0)) {
|
|
657
|
+
latestCompleteRow = row;
|
|
631
658
|
latestMemorySummary = perRow.memorySummary;
|
|
632
659
|
latestStrategyFilteredRefs = perRow.strategyFilteredRefs;
|
|
633
660
|
}
|
|
@@ -681,8 +708,18 @@ export function summarizePhaseDurations(samples) {
|
|
|
681
708
|
* Used by `akm health --detail per-run`.
|
|
682
709
|
*/
|
|
683
710
|
export function projectImproveRunSummary(row, wallTimeMs, taskId) {
|
|
684
|
-
|
|
685
|
-
|
|
711
|
+
let result = {};
|
|
712
|
+
let resultStatus = "invalid";
|
|
713
|
+
try {
|
|
714
|
+
const decoded = decodeImproveResult(row.result_json);
|
|
715
|
+
result = decoded.envelope;
|
|
716
|
+
resultStatus = decoded.normalizedLegacyPartial ? "normalized" : "valid";
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
// Keep the persisted row visible in per-run output, but do not project its
|
|
720
|
+
// unknown payload or admit its duration to result-derived denominators.
|
|
721
|
+
wallTimeMs = 0;
|
|
722
|
+
}
|
|
686
723
|
const perRow = projectRunMetrics(result);
|
|
687
724
|
finalizeImproveMetrics(perRow);
|
|
688
725
|
const orphansPurged = toFiniteNumber(result.orphansPurged);
|
|
@@ -695,6 +732,8 @@ export function projectImproveRunSummary(row, wallTimeMs, taskId) {
|
|
|
695
732
|
completedAt: row.completed_at,
|
|
696
733
|
wallTimeMs,
|
|
697
734
|
ok: row.ok === 1,
|
|
735
|
+
resultStatus,
|
|
736
|
+
resultComplete: resultStatus === "valid" && result.terminated === undefined,
|
|
698
737
|
strategy: row.strategy,
|
|
699
738
|
legacyProfile: row.legacyProfile,
|
|
700
739
|
scope: {
|
|
@@ -38,6 +38,7 @@ export function renderRunsDetailMd(runs) {
|
|
|
38
38
|
"graph_f/e/r",
|
|
39
39
|
"orphans",
|
|
40
40
|
"lint_f/fl",
|
|
41
|
+
"result_status",
|
|
41
42
|
];
|
|
42
43
|
const rows = runs.map((r) => {
|
|
43
44
|
const totalActions = r.actions.reflect.ok +
|
|
@@ -66,6 +67,7 @@ export function renderRunsDetailMd(runs) {
|
|
|
66
67
|
`${r.graphExtraction.extractedFiles}/${r.graphExtraction.entities}/${r.graphExtraction.relations}`,
|
|
67
68
|
String(r.orphansPurged),
|
|
68
69
|
`${r.lintFixed}/${r.lintFlagged}`,
|
|
70
|
+
r.resultStatus ?? "valid",
|
|
69
71
|
];
|
|
70
72
|
});
|
|
71
73
|
return renderTable(headers, rows);
|
|
@@ -89,7 +91,14 @@ export function renderWindowCompareMd(windows, deltas) {
|
|
|
89
91
|
"improve.memoryInference.skippedNoFacts",
|
|
90
92
|
]);
|
|
91
93
|
const rows = [];
|
|
92
|
-
|
|
94
|
+
const paths = [
|
|
95
|
+
"runs",
|
|
96
|
+
"improve.resultRows.included",
|
|
97
|
+
"improve.resultRows.normalized",
|
|
98
|
+
"improve.resultRows.skipped.invalid",
|
|
99
|
+
...INTERESTING_DELTA_PATHS,
|
|
100
|
+
];
|
|
101
|
+
for (const path of paths) {
|
|
93
102
|
const values = windows.map((w) => String(readNumericPath(w, path)));
|
|
94
103
|
const delta = deltas?.[path];
|
|
95
104
|
let deltaStr = "—";
|
|
@@ -146,7 +146,7 @@ export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Da
|
|
|
146
146
|
});
|
|
147
147
|
const { withLogs: taskRowsWithLogs, backed: existingLogRows } = partitionLogBackedRows(taskRows, logsDb);
|
|
148
148
|
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
149
|
-
const activeRows = taskRows.filter((row) => row.status === "active");
|
|
149
|
+
const activeRows = taskRows.filter((row) => row.status === "active" && row.completed_at === null);
|
|
150
150
|
const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
|
|
151
151
|
const promptRows = taskRows.filter((row) => row.target_kind === "prompt");
|
|
152
152
|
const promptFailures = promptRows.filter((row) => {
|
package/dist/commands/health.js
CHANGED
|
@@ -94,7 +94,7 @@ export function akmHealth(options = {}) {
|
|
|
94
94
|
const taskRows = queryTaskHistory(db, { since });
|
|
95
95
|
const { withLogs: taskRowsWithLogs, backed: existingLogRows } = partitionLogBackedRows(taskRows, logsDb);
|
|
96
96
|
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
97
|
-
const activeRows = taskRows.filter((row) => row.status === "active");
|
|
97
|
+
const activeRows = taskRows.filter((row) => row.status === "active" && row.completed_at === null);
|
|
98
98
|
const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
|
|
99
99
|
const promptRows = taskRows.filter((row) => row.target_kind === "prompt");
|
|
100
100
|
const promptFailures = promptRows.filter((row) => {
|
|
@@ -67,12 +67,12 @@ export const DEFAULT_IMPROVE_TASKS = [
|
|
|
67
67
|
},
|
|
68
68
|
];
|
|
69
69
|
/**
|
|
70
|
-
* A schedule for the manual catch-up task. The scheduler requires a
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
70
|
+
* A schedule for the manual catch-up task. The scheduler requires a portable
|
|
71
|
+
* expression even when the task is registered disabled, so we give it a
|
|
72
|
+
* nominal daily cadence and leave it disabled. The documented entry point is
|
|
73
|
+
* `akm tasks run akm-improve-catchup`.
|
|
74
74
|
*/
|
|
75
|
-
const MANUAL_TASK_NOMINAL_SCHEDULE = "0 4
|
|
75
|
+
const MANUAL_TASK_NOMINAL_SCHEDULE = "0 4 * * *";
|
|
76
76
|
const DEFAULT_DEPS = {
|
|
77
77
|
list: akmTasksList,
|
|
78
78
|
add: akmTasksAdd,
|
|
@@ -136,11 +136,15 @@ const tasksRunCommand = defineCommand({
|
|
|
136
136
|
name: "run",
|
|
137
137
|
description: "Execute a task now (this is what cron / launchd / schtasks invoke at the scheduled time)",
|
|
138
138
|
},
|
|
139
|
-
args: {
|
|
139
|
+
args: {
|
|
140
|
+
id: { type: "positional", description: "Task id", required: true },
|
|
141
|
+
scheduled: { type: "boolean", description: "Internal marker for scheduler-generated runs", default: false },
|
|
142
|
+
},
|
|
140
143
|
async run({ args }) {
|
|
141
144
|
await runWithJsonErrors(async () => {
|
|
142
|
-
const
|
|
143
|
-
|
|
145
|
+
const envelope = await akmTasksRun(args.id, {
|
|
146
|
+
scheduled: args.scheduled === true,
|
|
147
|
+
});
|
|
144
148
|
output("tasks-run", envelope);
|
|
145
149
|
if (envelope.exitCode !== 0)
|
|
146
150
|
process.exit(envelope.exitCode);
|