akm-cli 0.9.0-beta.5 → 0.9.0-beta.50
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 +709 -0
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +6 -2
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +17 -10
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +15 -5
- package/dist/commands/agent/agent-dispatch.js +2 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +17 -4
- package/dist/commands/env/env-cli.js +16 -24
- package/dist/commands/env/secret-cli.js +12 -20
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +422 -80
- package/dist/commands/health.js +386 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +81 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +635 -660
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +159 -69
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +124 -2
- package/dist/commands/improve/extract-prompt.js +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +389 -40
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +109 -6
- package/dist/commands/improve/improve-cli.js +35 -60
- package/dist/commands/improve/improve-profiles.js +14 -0
- package/dist/commands/improve/improve-result-file.js +5 -23
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2498
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1083 -0
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/preparation.js +1966 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +813 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +183 -40
- package/dist/commands/improve/salience.js +438 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +34 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +43 -50
- package/dist/commands/proposal/proposal-cli.js +21 -31
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +7 -2
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +189 -63
- package/dist/commands/read/curate.js +414 -94
- package/dist/commands/read/knowledge.js +2 -2
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/read/show.js +67 -2
- package/dist/commands/sources/init.js +36 -9
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/schema-repair.js +13 -1
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/stash-cli.js +28 -40
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/commands/tasks/tasks-cli.js +19 -27
- package/dist/commands/tasks/tasks.js +1 -1
- package/dist/commands/wiki-cli.js +21 -35
- package/dist/core/asset/asset-registry.js +2 -0
- package/dist/core/asset/asset-spec.js +14 -0
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +0 -5
- package/dist/core/config/config-schema.js +340 -56
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +28 -7
- package/dist/core/events.js +3 -7
- package/dist/core/improve-types.js +11 -8
- package/dist/core/logs-db.js +10 -66
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +3 -0
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +714 -0
- package/dist/core/state-db.js +525 -474
- package/dist/indexer/db/db.js +439 -247
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +218 -4
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +123 -221
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +10 -3
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/search/search-source.js +10 -24
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/config.js +6 -53
- package/dist/integrations/agent/index.js +2 -18
- package/dist/integrations/agent/prompts.js +74 -8
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +45 -15
- package/dist/llm/embedder.js +42 -3
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer.js +38 -30
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +73 -1
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/text/helpers.js +16 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +22 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2136 -1596
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +682 -433
- package/dist/setup/setup.js +29 -8
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +206 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/sources/providers/website.js +2 -3
- package/dist/sources/website-ingest.js +51 -9
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/registry-cache.js +92 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +5 -13
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +1 -117
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +9 -7
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
|
@@ -6,20 +6,27 @@ Schema: {{SCHEMA_PATH}}
|
|
|
6
6
|
Follow these steps. akm commands handle the invariants; use your native
|
|
7
7
|
Read/Write/Edit tools for page edits.
|
|
8
8
|
|
|
9
|
+
This workflow is for ingesting sources that are ALREADY present under
|
|
10
|
+
`{{WIKI_DIR}}/raw/`. Do not ask the user for a source unless the raw queue is
|
|
11
|
+
empty and the caller explicitly asked for interactive ingest.
|
|
12
|
+
|
|
9
13
|
1. **Read the schema.** Open `{{SCHEMA_PATH}}`. It defines the voice, page
|
|
10
14
|
kinds, contradiction policy, and any wiki-specific conventions. Do not
|
|
11
15
|
skip this step even on familiar wikis — the schema may have changed.
|
|
12
16
|
|
|
13
|
-
2. **
|
|
17
|
+
2. **Discover the pending raw queue.**
|
|
14
18
|
```sh
|
|
15
|
-
akm wiki
|
|
16
|
-
# or: cat <source> | akm wiki stash {{WIKI_NAME}} -
|
|
19
|
+
akm wiki lint {{WIKI_NAME}}
|
|
17
20
|
```
|
|
18
|
-
|
|
21
|
+
Focus on `uncited-raw` findings: those raw files exist under `raw/` but are
|
|
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.
|
|
19
25
|
|
|
20
|
-
3. **
|
|
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:
|
|
21
28
|
```sh
|
|
22
|
-
akm wiki search {{WIKI_NAME}} "<key terms from the source>"
|
|
29
|
+
akm wiki search {{WIKI_NAME}} "<key terms from the raw source>"
|
|
23
30
|
```
|
|
24
31
|
Read the top hits with `akm show wiki:{{WIKI_NAME}}/<page>`. Use
|
|
25
32
|
`akm show wiki:{{WIKI_NAME}}/<page> toc` for large pages.
|
|
@@ -39,8 +46,8 @@ Read/Write/Edit tools for page edits.
|
|
|
39
46
|
6. **Update xrefs both ways.** If page A now xrefs page B, page B must xref
|
|
40
47
|
page A. `akm wiki lint {{WIKI_NAME}}` will flag violations.
|
|
41
48
|
|
|
42
|
-
7. **Append to `log.md`.** One entry per
|
|
43
|
-
summary, refs to created/edited pages. Newest at the top.
|
|
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.
|
|
44
51
|
|
|
45
52
|
8. **Regenerate the index + verify.**
|
|
46
53
|
```sh
|
|
@@ -50,5 +57,5 @@ Read/Write/Edit tools for page edits.
|
|
|
50
57
|
Resolve any lint findings before calling the ingest done.
|
|
51
58
|
|
|
52
59
|
That's it. `akm` never calls an LLM — reasoning is your job; it just owns
|
|
53
|
-
the invariants (raw immutability,
|
|
54
|
-
|
|
60
|
+
the invariants (raw immutability, ref validation, index regeneration,
|
|
61
|
+
structural lint).
|
package/dist/cli/shared.js
CHANGED
|
@@ -15,6 +15,7 @@ import { getOutputMode } from "../output/context.js";
|
|
|
15
15
|
import { DEFAULT_TEMPLATE, deliverRendered, escapeHtml, renderHtml, resolveTemplatePath } from "../output/html-render.js";
|
|
16
16
|
import { shapeForCommand } from "../output/shapes.js";
|
|
17
17
|
import { formatPlain, outputJsonl } from "../output/text.js";
|
|
18
|
+
import { hasSubcommand } from "./parse-args.js";
|
|
18
19
|
// ── Exit codes ───────────────────────────────────────────────────────────────
|
|
19
20
|
/**
|
|
20
21
|
* Canonical process exit-code table for the akm CLI. Single source of truth —
|
|
@@ -111,6 +112,33 @@ export function defineJsonCommand(def) {
|
|
|
111
112
|
run: (context) => runWithJsonErrors(() => run(context)),
|
|
112
113
|
});
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Define a citty subcommand-group command (env, secret, proposal, tasks, wiki,
|
|
117
|
+
* graph, …) that shares one wiring shape: a `subCommands` map, a routing set
|
|
118
|
+
* DERIVED from that map's keys (so the set can never silently desync from the
|
|
119
|
+
* registered subcommands), and a default body that fires ONLY for the bare
|
|
120
|
+
* group invocation — citty still runs the group body after dispatching a
|
|
121
|
+
* subcommand, so the shared guard short-circuits when `args._[0]` names a
|
|
122
|
+
* registered subcommand.
|
|
123
|
+
*
|
|
124
|
+
* The `defaultRun` body is wrapped in `runWithJsonErrors`, so it emits a
|
|
125
|
+
* byte-identical JSON error envelope on throw — exactly the per-site
|
|
126
|
+
* `run() { return runWithJsonErrors(() => { if (hasSubcommand(...)) return; … }); }`
|
|
127
|
+
* boilerplate this replaces.
|
|
128
|
+
*/
|
|
129
|
+
export function defineGroupCommand(def) {
|
|
130
|
+
const subcommandSet = new Set(Object.keys(def.subCommands));
|
|
131
|
+
return defineCommand({
|
|
132
|
+
meta: def.meta,
|
|
133
|
+
...(def.args ? { args: def.args } : {}),
|
|
134
|
+
subCommands: def.subCommands,
|
|
135
|
+
run: (context) => runWithJsonErrors(() => {
|
|
136
|
+
if (hasSubcommand(context.args, subcommandSet))
|
|
137
|
+
return;
|
|
138
|
+
return def.defaultRun(context);
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
114
142
|
/**
|
|
115
143
|
* Render a command result according to the active output mode
|
|
116
144
|
* (json/jsonl/yaml/text/md/html). When `--output <path>` is set, the rendered
|
package/dist/cli.js
CHANGED
|
@@ -77,7 +77,7 @@ import { registryCommand } from "./commands/registry-cli.js";
|
|
|
77
77
|
import { addCommand } from "./commands/sources/add-cli.js";
|
|
78
78
|
import { renderMigrationHelp } from "./commands/sources/migration-help.js";
|
|
79
79
|
import { cloneCommand, historyCommand, listCommand, removeCommand, syncCommand, updateCommand, upgradeCommand, } from "./commands/sources/sources-cli.js";
|
|
80
|
-
import {
|
|
80
|
+
import { importKnowledgeCommand, indexCommand, infoCommand, initCommand } from "./commands/sources/stash-cli.js";
|
|
81
81
|
import { tasksCommand } from "./commands/tasks/tasks-cli.js";
|
|
82
82
|
import { wikiCommand } from "./commands/wiki-cli.js";
|
|
83
83
|
import { workflowCommand } from "./commands/workflow-cli.js";
|
|
@@ -320,10 +320,14 @@ const healthCommand = defineCommand({
|
|
|
320
320
|
// window-compare read for the trend deltas (defaults to 24h,
|
|
321
321
|
// overridable via --compare).
|
|
322
322
|
if (mode.format === "html") {
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
// Default the compare window to the report's own `--since` window so the
|
|
324
|
+
// trend deltas are like-for-like (e.g. last 7d vs the prior 7d). A fixed
|
|
325
|
+
// 24h default made a `--since 7d` report compare its 7-day totals against
|
|
326
|
+
// a 24-hour prior window, producing meaningless deltas.
|
|
327
|
+
const compare = args.compare ?? windowCompareRaw ?? args.since ?? "24h";
|
|
328
|
+
const result = akmHealth({ since: args.since, groupBy: "run", windowCompare: compare });
|
|
325
329
|
resultStatus = result.status;
|
|
326
|
-
const deltas =
|
|
330
|
+
const deltas = result.deltas;
|
|
327
331
|
const { buildHealthHtmlReplacements } = await import("./commands/health/html-report.js");
|
|
328
332
|
const { listPendingProposals } = await import("./commands/proposal/proposal.js");
|
|
329
333
|
const replacements = buildHealthHtmlReplacements(result, {
|
|
@@ -482,7 +486,6 @@ export const main = defineCommand({
|
|
|
482
486
|
health: healthCommand,
|
|
483
487
|
info: infoCommand,
|
|
484
488
|
graph: graphCommand,
|
|
485
|
-
db: dbCommand,
|
|
486
489
|
add: addCommand,
|
|
487
490
|
list: listCommand,
|
|
488
491
|
remove: removeCommand,
|
|
@@ -536,6 +539,13 @@ const EXIT_HEALTH_WARN = EXIT_CODES.HEALTH_WARN;
|
|
|
536
539
|
// The wrapper sets `AKM_NODE_ENTRY=1` to opt into the startup block. The test
|
|
537
540
|
// harness never sets it, so importing cli.ts under Bun stays inert as before.
|
|
538
541
|
if (import.meta.main || process.env.AKM_NODE_ENTRY === "1") {
|
|
542
|
+
// Mark that this process is the real akm CLI: its `process.argv[1]` is the
|
|
543
|
+
// akm entrypoint, so the background auto-reindex may safely re-invoke it as a
|
|
544
|
+
// detached child. Hosts that merely import this module (the in-process test
|
|
545
|
+
// harness, library embeddings) never reach this block, so they fall back to
|
|
546
|
+
// an inline reindex instead of spawning the wrong program. See
|
|
547
|
+
// `ensureIndex` in src/indexer/ensure-index.ts.
|
|
548
|
+
process.env.AKM_CLI_ENTRY = "1";
|
|
539
549
|
// citty reads process.argv directly and does not accept a custom argv array,
|
|
540
550
|
// so we must replace process.argv with the normalized version before runMain.
|
|
541
551
|
process.argv = normalizeShowArgv(process.argv);
|
|
@@ -18,7 +18,7 @@ import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
|
18
18
|
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
19
19
|
import { requireAgentProfile } from "../../integrations/agent/config.js";
|
|
20
20
|
import { runAgent } from "../../integrations/agent/spawn.js";
|
|
21
|
-
import {
|
|
21
|
+
import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
|
|
22
22
|
/**
|
|
23
23
|
* Fill `{{0}}`, `{{1}}`, ... placeholders in `template` with the
|
|
24
24
|
* corresponding entries in `args`. Any placeholder index that exceeds the
|
|
@@ -93,7 +93,7 @@ export async function akmAgentDispatch(options) {
|
|
|
93
93
|
...(dispatchRequest !== undefined ? { dispatch: dispatchRequest } : {}),
|
|
94
94
|
};
|
|
95
95
|
const result = profile.sdkMode
|
|
96
|
-
? await
|
|
96
|
+
? await runOpencodeSdk(profile, prompt ?? "", runOptions, options.llmConfig)
|
|
97
97
|
: await runAgent(profile, prompt, runOptions);
|
|
98
98
|
return {
|
|
99
99
|
schemaVersion: 1,
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
* Shared helpers for agent-based commands (reflect, propose, etc.).
|
|
6
|
-
*
|
|
7
|
-
* Consolidates utility functions that were duplicated byte-for-byte across
|
|
8
|
-
* `reflect.ts` and `propose.ts`. Any command that shells out to an agent
|
|
9
|
-
* profile can import from here rather than copy-pasting.
|
|
10
|
-
*/
|
|
11
4
|
import { loadConfig } from "../../core/config/config.js";
|
|
12
5
|
import { requireAgentProfile, } from "../../integrations/agent/index.js";
|
|
13
6
|
// ── Config helpers ───────────────────────────────────────────────────────────
|
|
@@ -29,7 +29,7 @@ import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "
|
|
|
29
29
|
import { loadConfig } from "../../core/config/config.js";
|
|
30
30
|
import { UsageError } from "../../core/errors.js";
|
|
31
31
|
import { getHyphenatedArg } from "../../output/context.js";
|
|
32
|
-
import { akmLint } from "../lint.js";
|
|
32
|
+
import { akmLint } from "../lint/index.js";
|
|
33
33
|
import { akmPropose } from "../proposal/propose.js";
|
|
34
34
|
import { akmAgentDispatch } from "./agent-dispatch.js";
|
|
35
35
|
const EXIT_GENERAL = EXIT_CODES.GENERAL;
|
|
@@ -122,23 +122,36 @@ export const lintCommand = defineCommand({
|
|
|
122
122
|
description: "Scan stash .md files for structural issues (unquoted colons, missing updated field, orphaned stubs, placeholder stubs, missing name/type, stale paths). Use --fix to auto-fix Tier 1 issues. Exits 0 on success regardless of findings; use --fail-on-flagged for CI fail-on-finding behavior.",
|
|
123
123
|
},
|
|
124
124
|
args: {
|
|
125
|
-
fix: {
|
|
125
|
+
fix: {
|
|
126
|
+
type: "boolean",
|
|
127
|
+
alias: "auto-fix",
|
|
128
|
+
description: "Apply auto-fixes in place (alias: --auto-fix)",
|
|
129
|
+
default: false,
|
|
130
|
+
},
|
|
126
131
|
dir: { type: "string", description: "Override stash root directory (default: from config)" },
|
|
127
132
|
"fail-on-flagged": {
|
|
128
133
|
type: "boolean",
|
|
129
134
|
description: "Exit non-zero when summary.flagged > 0 (CI-friendly). Default: exit 0 regardless of findings.",
|
|
130
135
|
default: false,
|
|
131
136
|
},
|
|
137
|
+
type: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description: "Only lint assets of this type (e.g. workflows, tasks, memories)",
|
|
140
|
+
default: undefined,
|
|
141
|
+
},
|
|
132
142
|
},
|
|
133
143
|
async run({ args }) {
|
|
134
144
|
await runWithJsonErrors(async () => {
|
|
135
145
|
const result = akmLint({
|
|
136
146
|
fix: args.fix ?? false,
|
|
137
147
|
dir: getStringArg(args, "dir"),
|
|
148
|
+
typeFilter: getStringArg(args, "type"),
|
|
138
149
|
});
|
|
139
150
|
output("lint", result);
|
|
140
|
-
if (args["fail-on-flagged"] && result.summary.flagged > 0)
|
|
141
|
-
process.
|
|
151
|
+
if (args["fail-on-flagged"] && result.summary.flagged > 0) {
|
|
152
|
+
process.exitCode = EXIT_GENERAL;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
142
155
|
});
|
|
143
156
|
},
|
|
144
157
|
});
|
|
@@ -19,8 +19,8 @@ import { spawnSync } from "node:child_process";
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { defineCommand } from "citty";
|
|
22
|
-
import { getStringArg
|
|
23
|
-
import { output, runWithJsonErrors } from "../../cli/shared.js";
|
|
22
|
+
import { getStringArg } from "../../cli/parse-args.js";
|
|
23
|
+
import { defineGroupCommand, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
24
24
|
import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../../core/asset/asset-create.js";
|
|
25
25
|
import { deriveCanonicalAssetName, resolveAssetPathFromName } from "../../core/asset/asset-spec.js";
|
|
26
26
|
import { isWithin, writeFileAtomic } from "../../core/common.js";
|
|
@@ -499,31 +499,23 @@ const envUnsetCommand = defineCommand({
|
|
|
499
499
|
});
|
|
500
500
|
},
|
|
501
501
|
});
|
|
502
|
-
|
|
503
|
-
// (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
|
|
504
|
-
const envSubCommands = {
|
|
505
|
-
list: envListCommand,
|
|
506
|
-
path: envPathCommand,
|
|
507
|
-
export: envExportCommand,
|
|
508
|
-
run: envRunCommand,
|
|
509
|
-
create: envCreateCommand,
|
|
510
|
-
set: envSetCommand,
|
|
511
|
-
unset: envUnsetCommand,
|
|
512
|
-
remove: envRemoveCommand,
|
|
513
|
-
};
|
|
514
|
-
const ENV_SUBCOMMAND_SET = new Set(Object.keys(envSubCommands));
|
|
515
|
-
export const envCommand = defineCommand({
|
|
502
|
+
export const envCommand = defineGroupCommand({
|
|
516
503
|
meta: {
|
|
517
504
|
name: "env",
|
|
518
505
|
description: "Manage `.env` files — a group of related CONFIGURATION values for an app or service (URLs, flags, plus any credentials it needs), loaded together. Values may or may not be sensitive; akm protects them all the same (key names visible, values never in structured output). For a single sensitive value used on its own (an auth token, key, or cert), use `akm secret`.",
|
|
519
506
|
},
|
|
520
|
-
subCommands:
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
507
|
+
subCommands: {
|
|
508
|
+
list: envListCommand,
|
|
509
|
+
path: envPathCommand,
|
|
510
|
+
export: envExportCommand,
|
|
511
|
+
run: envRunCommand,
|
|
512
|
+
create: envCreateCommand,
|
|
513
|
+
set: envSetCommand,
|
|
514
|
+
unset: envUnsetCommand,
|
|
515
|
+
remove: envRemoveCommand,
|
|
516
|
+
},
|
|
517
|
+
async defaultRun() {
|
|
518
|
+
const { listKeys } = await import("./env.js");
|
|
519
|
+
output("env-list", { envs: listEnvsRecursive(listKeys) });
|
|
528
520
|
},
|
|
529
521
|
});
|
|
@@ -19,8 +19,8 @@ import { spawnSync } from "node:child_process";
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { defineCommand } from "citty";
|
|
22
|
-
import { getStringArg
|
|
23
|
-
import { output, runWithJsonErrors } from "../../cli/shared.js";
|
|
22
|
+
import { getStringArg } from "../../cli/parse-args.js";
|
|
23
|
+
import { defineGroupCommand, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
24
24
|
import { deriveCanonicalAssetName } from "../../core/asset/asset-spec.js";
|
|
25
25
|
import { loadConfig } from "../../core/config/config.js";
|
|
26
26
|
import { makeSecretRef, resolveSecretPath } from "../../core/env-secret-ref.js";
|
|
@@ -233,27 +233,19 @@ const secretRemoveCommand = defineCommand({
|
|
|
233
233
|
});
|
|
234
234
|
},
|
|
235
235
|
});
|
|
236
|
-
|
|
237
|
-
// (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
|
|
238
|
-
const secretSubCommands = {
|
|
239
|
-
list: secretListCommand,
|
|
240
|
-
path: secretPathCommand,
|
|
241
|
-
run: secretRunCommand,
|
|
242
|
-
set: secretSetCommand,
|
|
243
|
-
remove: secretRemoveCommand,
|
|
244
|
-
};
|
|
245
|
-
const SECRET_SUBCOMMAND_SET = new Set(Object.keys(secretSubCommands));
|
|
246
|
-
export const secretCommand = defineCommand({
|
|
236
|
+
export const secretCommand = defineGroupCommand({
|
|
247
237
|
meta: {
|
|
248
238
|
name: "secret",
|
|
249
239
|
description: "Manage secrets — a single sensitive value used on its own for authentication (an API token, a PEM private key, a TLS cert), one value per file. Names are visible; the file contents are the value and never appear in structured output. For a group of related configuration loaded together, use `akm env`.",
|
|
250
240
|
},
|
|
251
|
-
subCommands:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
241
|
+
subCommands: {
|
|
242
|
+
list: secretListCommand,
|
|
243
|
+
path: secretPathCommand,
|
|
244
|
+
run: secretRunCommand,
|
|
245
|
+
set: secretSetCommand,
|
|
246
|
+
remove: secretRemoveCommand,
|
|
247
|
+
},
|
|
248
|
+
defaultRun() {
|
|
249
|
+
output("secret-list", { secrets: listSecretsRecursive() });
|
|
258
250
|
},
|
|
259
251
|
});
|
|
@@ -11,10 +11,9 @@ import { writeFileAtomic } from "../core/common.js";
|
|
|
11
11
|
import { FEEDBACK_FAILURE_MODES, loadConfig } from "../core/config/config.js";
|
|
12
12
|
import { UsageError } from "../core/errors.js";
|
|
13
13
|
import { appendEvent } from "../core/events.js";
|
|
14
|
+
import { getDbPath } from "../core/paths.js";
|
|
14
15
|
import { warn } from "../core/warn.js";
|
|
15
16
|
import { applyFeedbackToUtilityScore, closeDatabase, findEntryIdByRef, getEntryFilePathById, openExistingDatabase, } from "../indexer/db/db.js";
|
|
16
|
-
import { ensureIndex } from "../indexer/ensure-index.js";
|
|
17
|
-
import { resolveSourceEntries } from "../indexer/search/search-source.js";
|
|
18
17
|
import { countFeedbackSignals, insertUsageEvent } from "../indexer/usage/usage-events.js";
|
|
19
18
|
// ── Tag validation ────────────────────────────────────────────────────────────
|
|
20
19
|
const TAG_KEY_RE = /^[a-z_][a-z0-9_]*$/;
|
|
@@ -203,11 +202,21 @@ export const feedbackCommand = defineCommand({
|
|
|
203
202
|
...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
|
|
204
203
|
};
|
|
205
204
|
const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
// Feedback only needs the index to exist, not to be current. A stale index
|
|
206
|
+
// is fine — the ref lookup works against any populated DB. We do NOT call
|
|
207
|
+
// ensureIndex here: it either blocks (3+ min inline reindex) or spawns a
|
|
208
|
+
// background process that holds the writer lock, causing the feedback write
|
|
209
|
+
// to spin-wait for the full reindex duration. If the DB is absent we give a
|
|
210
|
+
// clear error below rather than silently triggering a rebuild.
|
|
211
|
+
if (!fs.existsSync(getDbPath())) {
|
|
212
|
+
throw new UsageError("Index not found. Run 'akm index' first to build the index before recording feedback.", "MISSING_REQUIRED_ARGUMENT", "akm index");
|
|
210
213
|
}
|
|
214
|
+
// Feedback writes exactly 2 rows (usage_events + utility_score). SQLite
|
|
215
|
+
// WAL mode + busy_timeout=30s handles concurrent access with an ongoing
|
|
216
|
+
// `akm improve` run without needing the application-level writer lock.
|
|
217
|
+
// The lock was originally needed to prevent feedback from racing a
|
|
218
|
+
// background reindex it spawned — now that ensureIndex is removed, holding
|
|
219
|
+
// the lock only causes feedback to block for the full improve run duration.
|
|
211
220
|
let utilityResult;
|
|
212
221
|
const db = openExistingDatabase();
|
|
213
222
|
try {
|
|
@@ -9,12 +9,9 @@
|
|
|
9
9
|
* same JSON envelope (stdout/stderr/exit-code) as the inline `runWithJsonErrors`
|
|
10
10
|
* form it replaces.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { defineJsonCommand, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
12
|
+
import { parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
13
|
+
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
|
|
15
14
|
import { akmGraphEntities, akmGraphEntity, akmGraphExport, akmGraphOrphans, akmGraphRelated, akmGraphRelations, akmGraphSummary, akmGraphUpdate, } from "./graph.js";
|
|
16
|
-
// Single source of truth: the routing set is derived from the subCommands keys
|
|
17
|
-
// (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
|
|
18
15
|
const graphSubCommands = {
|
|
19
16
|
summary: defineJsonCommand({
|
|
20
17
|
meta: { name: "summary", description: "Show entity-graph counts and quality telemetry" },
|
|
@@ -118,15 +115,10 @@ const graphSubCommands = {
|
|
|
118
115
|
},
|
|
119
116
|
}),
|
|
120
117
|
};
|
|
121
|
-
const
|
|
122
|
-
export const graphCommand = defineCommand({
|
|
118
|
+
export const graphCommand = defineGroupCommand({
|
|
123
119
|
meta: { name: "graph", description: "Inspect the indexed entity graph stored in SQLite" },
|
|
124
120
|
subCommands: graphSubCommands,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (hasSubcommand(args, GRAPH_SUBCOMMAND_SET))
|
|
128
|
-
return;
|
|
129
|
-
output("graph-summary", akmGraphSummary());
|
|
130
|
-
});
|
|
121
|
+
defaultRun() {
|
|
122
|
+
output("graph-summary", akmGraphSummary());
|
|
131
123
|
},
|
|
132
124
|
});
|
|
@@ -8,10 +8,11 @@ import { loadConfig } from "../../core/config/config.js";
|
|
|
8
8
|
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
9
9
|
import { getDbPath } from "../../core/paths.js";
|
|
10
10
|
import { warn } from "../../core/warn.js";
|
|
11
|
-
import { closeDatabase, findEntryIdByRef, getEntryById, getEntryRefRowsForStashRoot,
|
|
11
|
+
import { closeDatabase, findEntryIdByRef, getEntryById, getEntryRefRowsForStashRoot, openExistingDatabase, openIndexDatabase, } from "../../indexer/db/db.js";
|
|
12
12
|
import { loadStoredGraphSnapshot } from "../../indexer/db/graph-db.js";
|
|
13
13
|
import { listRelatedPathsForFile } from "../../indexer/graph/graph-boost.js";
|
|
14
14
|
import { runGraphExtractionPass } from "../../indexer/graph/graph-extraction.js";
|
|
15
|
+
import { withIndexWriterLease } from "../../indexer/index-writer-lock.js";
|
|
15
16
|
import { lookup } from "../../indexer/indexer.js";
|
|
16
17
|
import { findSourceForPath, resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
17
18
|
import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
|
|
@@ -375,85 +376,88 @@ export async function akmGraphUpdate(options) {
|
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
const scoped = Array.isArray(options.refs) && options.refs.length > 0;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
379
|
+
return withIndexWriterLease({ purpose: "graph-update" }, async () => {
|
|
380
|
+
let candidatePaths;
|
|
381
|
+
if (scoped && options.refs) {
|
|
382
|
+
// Resolve each ref to an absolute file path while the writer lease is held
|
|
383
|
+
// so the scoped graph write sees the same index snapshot it resolved from.
|
|
384
|
+
const dbPath = getDbPath();
|
|
385
|
+
let db;
|
|
386
|
+
const resolvedPaths = new Set();
|
|
387
|
+
try {
|
|
388
|
+
db = openIndexDatabase(dbPath);
|
|
389
|
+
for (const ref of options.refs) {
|
|
390
|
+
const trimmed = ref.trim();
|
|
391
|
+
if (!trimmed)
|
|
392
|
+
continue;
|
|
393
|
+
const entryId = findEntryIdByRef(db, trimmed);
|
|
394
|
+
if (entryId === undefined) {
|
|
395
|
+
warn(`[graph] ref not found in index, skipping: ${trimmed}`);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const row = getEntryById(db, entryId);
|
|
399
|
+
if (!row?.filePath) {
|
|
400
|
+
warn(`[graph] could not resolve path for ref, skipping: ${trimmed}`);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
resolvedPaths.add(row.filePath);
|
|
394
404
|
}
|
|
395
|
-
const row = getEntryById(db, entryId);
|
|
396
|
-
if (!row?.filePath) {
|
|
397
|
-
warn(`[graph] could not resolve path for ref, skipping: ${trimmed}`);
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
resolvedPaths.add(row.filePath);
|
|
401
405
|
}
|
|
406
|
+
finally {
|
|
407
|
+
if (db)
|
|
408
|
+
closeDatabase(db);
|
|
409
|
+
}
|
|
410
|
+
if (resolvedPaths.size === 0) {
|
|
411
|
+
warn("[graph] none of the provided refs resolved to indexed paths — no extraction performed.");
|
|
412
|
+
return {
|
|
413
|
+
shape: "graph-update",
|
|
414
|
+
ok: true,
|
|
415
|
+
filesExtracted: 0,
|
|
416
|
+
entitiesUpserted: 0,
|
|
417
|
+
relationsUpserted: 0,
|
|
418
|
+
durationMs: 0,
|
|
419
|
+
scoped: true,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
candidatePaths = resolvedPaths;
|
|
402
423
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
424
|
+
const extractionFn = options.graphExtractionFn ?? runGraphExtractionPass;
|
|
425
|
+
const passOptions = candidatePaths ? { candidatePaths } : {};
|
|
426
|
+
let db;
|
|
427
|
+
const startMs = Date.now();
|
|
428
|
+
try {
|
|
429
|
+
db = openIndexDatabase(getDbPath());
|
|
430
|
+
const onProgress = (event) => {
|
|
431
|
+
if (!event.currentPath)
|
|
432
|
+
return;
|
|
433
|
+
const file = path.basename(event.currentPath);
|
|
434
|
+
warn(`[graph] extracting ${event.processed}/${event.total} ${file}`);
|
|
435
|
+
};
|
|
436
|
+
const result = await extractionFn({
|
|
437
|
+
config,
|
|
438
|
+
sources,
|
|
439
|
+
signal: undefined,
|
|
440
|
+
db,
|
|
441
|
+
reEnrich: false,
|
|
442
|
+
onProgress,
|
|
443
|
+
options: passOptions,
|
|
444
|
+
});
|
|
445
|
+
const durationMs = Date.now() - startMs;
|
|
409
446
|
return {
|
|
410
447
|
shape: "graph-update",
|
|
411
448
|
ok: true,
|
|
412
|
-
filesExtracted:
|
|
413
|
-
entitiesUpserted:
|
|
414
|
-
relationsUpserted:
|
|
415
|
-
durationMs
|
|
416
|
-
scoped
|
|
449
|
+
filesExtracted: result.quality.extractedFiles,
|
|
450
|
+
entitiesUpserted: result.quality.entityCount,
|
|
451
|
+
relationsUpserted: result.quality.relationCount,
|
|
452
|
+
durationMs,
|
|
453
|
+
scoped,
|
|
417
454
|
};
|
|
418
455
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const startMs = Date.now();
|
|
425
|
-
try {
|
|
426
|
-
db = openDatabase(getDbPath());
|
|
427
|
-
const onProgress = (event) => {
|
|
428
|
-
if (!event.currentPath)
|
|
429
|
-
return;
|
|
430
|
-
const file = path.basename(event.currentPath);
|
|
431
|
-
warn(`[graph] extracting ${event.processed}/${event.total} ${file}`);
|
|
432
|
-
};
|
|
433
|
-
const result = await extractionFn({
|
|
434
|
-
config,
|
|
435
|
-
sources,
|
|
436
|
-
signal: undefined,
|
|
437
|
-
db,
|
|
438
|
-
reEnrich: false,
|
|
439
|
-
onProgress,
|
|
440
|
-
options: passOptions,
|
|
441
|
-
});
|
|
442
|
-
const durationMs = Date.now() - startMs;
|
|
443
|
-
return {
|
|
444
|
-
shape: "graph-update",
|
|
445
|
-
ok: true,
|
|
446
|
-
filesExtracted: result.quality.extractedFiles,
|
|
447
|
-
entitiesUpserted: result.quality.entityCount,
|
|
448
|
-
relationsUpserted: result.quality.relationCount,
|
|
449
|
-
durationMs,
|
|
450
|
-
scoped,
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
finally {
|
|
454
|
-
if (db)
|
|
455
|
-
closeDatabase(db);
|
|
456
|
-
}
|
|
456
|
+
finally {
|
|
457
|
+
if (db)
|
|
458
|
+
closeDatabase(db);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
457
461
|
}
|
|
458
462
|
async function resolveGraphTarget(ref, source) {
|
|
459
463
|
const parsedRef = parseAssetRef(ref);
|
|
@@ -257,6 +257,54 @@ export const HEALTH_CHECKS = [
|
|
|
257
257
|
};
|
|
258
258
|
},
|
|
259
259
|
},
|
|
260
|
+
{
|
|
261
|
+
// #603: pool-saturation advisory. The raw `sessionsScanned` count fired on
|
|
262
|
+
// normal cadence changes (the Jun 12 false alarm). Instead track the ratio
|
|
263
|
+
// of NEW (unseen) sessions to the total session pool extract evaluated in
|
|
264
|
+
// the window: a low ratio is the *expected* steady state, only a near-zero
|
|
265
|
+
// ratio signals a possible discovery/dedup bug.
|
|
266
|
+
//
|
|
267
|
+
// unseen ≈ `sessionsScanned` (extract only processes new sessions; already-
|
|
268
|
+
// seen ones are deduped into `sessionsSkipped`). total = scanned + skipped.
|
|
269
|
+
// This is a heuristic approximation — `sessionsSkipped` also folds in
|
|
270
|
+
// too-short skips — so the check is informational and never gates status.
|
|
271
|
+
name: "pool-saturation",
|
|
272
|
+
channel: "advisory",
|
|
273
|
+
run: (ctx) => {
|
|
274
|
+
const sx = ctx.sessionExtraction;
|
|
275
|
+
const total = sx.sessionsScanned + sx.sessionsSkipped;
|
|
276
|
+
const unseen = sx.sessionsScanned;
|
|
277
|
+
const ratio = total > 0 ? unseen / total : null;
|
|
278
|
+
const pct = ratio === null ? null : Math.round(ratio * 1000) / 10;
|
|
279
|
+
let status = "pass";
|
|
280
|
+
let confidence = "low";
|
|
281
|
+
let message;
|
|
282
|
+
if (!sx.ran || ratio === null) {
|
|
283
|
+
message = "Pool saturation: no extract activity in the window — no signal.";
|
|
284
|
+
}
|
|
285
|
+
else if (ratio < 0.02) {
|
|
286
|
+
status = "warn";
|
|
287
|
+
confidence = "medium";
|
|
288
|
+
message = `Session pool near-exhausted: only ${pct}% of the ${total}-session pool was new (<2%). Possible discovery/dedup bug — verify extract is still finding new sessions.`;
|
|
289
|
+
}
|
|
290
|
+
else if (ratio < 0.1) {
|
|
291
|
+
confidence = "medium";
|
|
292
|
+
message = `Session pool saturation: ${pct}% of ${total} sessions were new (<10%, steady-state expected — informational).`;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
confidence = "medium";
|
|
296
|
+
message = `Session pool healthy: ${pct}% of ${total} sessions were new.`;
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
name: "pool-saturation",
|
|
300
|
+
kind: "heuristic",
|
|
301
|
+
status,
|
|
302
|
+
confidence,
|
|
303
|
+
message,
|
|
304
|
+
evidence: { totalSessions: total, unseenSessions: unseen, saturationRatio: ratio },
|
|
305
|
+
};
|
|
306
|
+
},
|
|
307
|
+
},
|
|
260
308
|
{
|
|
261
309
|
name: "auto-accept-validation",
|
|
262
310
|
channel: "advisory",
|