akm-cli 0.9.0-beta.9 → 0.9.0-rc.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 +715 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- 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/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- 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 +11 -3
- 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/prompts/workflow-unit-preamble.md +26 -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 +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +228 -292
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -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 +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -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 +38 -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/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +291 -13
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +79 -5
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +403 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/json-schema.js +142 -0
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- 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 +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +261 -770
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +48 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +41 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +79 -6
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +83 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +206 -81
- package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
- package/dist/integrations/harnesses/aider/index.js +58 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
- package/dist/integrations/harnesses/amazonq/index.js +59 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -7
- package/dist/integrations/harnesses/claude/index.js +27 -23
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
- package/dist/integrations/harnesses/codex/index.js +63 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
- package/dist/integrations/harnesses/copilot/index.js +60 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
- package/dist/integrations/harnesses/gemini/index.js +60 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +28 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +17 -16
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +10 -34
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +642 -71
- package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
- package/dist/integrations/harnesses/openhands/index.js +58 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
- package/dist/integrations/harnesses/pi/index.js +58 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -0
- package/dist/integrations/session-logs/index.js +40 -11
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +87 -15
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +6 -1
- package/dist/output/text/helpers.js +241 -2
- package/dist/output/text/workflow.js +4 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/schemas/akm-config.json +14225 -0
- package/dist/schemas/akm-workflow.json +328 -0
- package/dist/scripts/migrate-storage.js +2743 -8390
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +1652 -607
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -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/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- 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/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/repositories/workflow-runs-repository.js +189 -1
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- 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 +15 -13
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +5 -0
- package/dist/workflows/db.js +138 -4
- package/dist/workflows/exec/brief.js +484 -0
- package/dist/workflows/exec/native-executor.js +975 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1295 -0
- package/dist/workflows/exec/run-workflow.js +596 -0
- package/dist/workflows/exec/scheduler.js +100 -0
- package/dist/workflows/exec/step-work.js +1156 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +388 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +33 -0
- package/dist/workflows/ir/schema.js +4 -0
- package/dist/workflows/parser.js +3 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +760 -0
- package/dist/workflows/program/project.js +105 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/runs.js +248 -153
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +188 -0
- package/dist/workflows/validate-summary.js +26 -10
- package/dist/workflows/validator.js +1 -1
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +7 -5
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
* The remaining `surfaces` advisory group for `akm health` (meta-review 08).
|
|
6
|
+
* `stash-git-exposure` (08-F1) shipped first in ./stash-exposure.ts; this
|
|
7
|
+
* module adds the other four read-only checks the adjudication approved:
|
|
8
|
+
*
|
|
9
|
+
* - `secret-file-perms` — env/secret/backup files not 0600, dirs not 0700 (F4)
|
|
10
|
+
* - `binary-config-skew` — config.json written by a NEWER akm than this binary (F3)
|
|
11
|
+
* - `orphan-stores` — legacy config-backups dirs + 0-byte stash state.db decoy (F4/F7)
|
|
12
|
+
* - `egress-endpoints` — the remote-destination list, for eyeball diff (surfaces 3/9)
|
|
13
|
+
*
|
|
14
|
+
* Every collector is a pure projection over injected paths/config (no
|
|
15
|
+
* process.env reads) and is silent when there is nothing to report, matching
|
|
16
|
+
* the stash-exposure pattern. `egress-endpoints` is the one informational
|
|
17
|
+
* (pass-status) entry: it emits whenever any remote endpoint is configured.
|
|
18
|
+
*/
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { CURRENT_CONFIG_VERSION, compareConfigVersion } from "../../core/config/config-migration.js";
|
|
22
|
+
const GROUP_OTHER_BITS = 0o077;
|
|
23
|
+
const OFFENDER_EVIDENCE_CAP = 50;
|
|
24
|
+
function modeOctal(mode) {
|
|
25
|
+
return (mode & 0o777).toString(8).padStart(3, "0");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `secret-file-perms` (08-F4): flag env/secret/backup files that are not 0600
|
|
29
|
+
* and their directories when not 0700. Scans `<stash>/env`, `<stash>/secrets`
|
|
30
|
+
* and `<cache>/config-backups`; anything readable by group/other is an
|
|
31
|
+
* offender. Silent when every path is tight (or none of the dirs exist).
|
|
32
|
+
*/
|
|
33
|
+
export function collectSecretPermsAdvisory(input, platform = process.platform) {
|
|
34
|
+
if (platform === "win32")
|
|
35
|
+
return undefined;
|
|
36
|
+
const roots = [
|
|
37
|
+
path.join(input.stashDir, "env"),
|
|
38
|
+
path.join(input.stashDir, "secrets"),
|
|
39
|
+
path.join(input.cacheDir, "config-backups"),
|
|
40
|
+
];
|
|
41
|
+
const offenders = [];
|
|
42
|
+
for (const root of roots) {
|
|
43
|
+
let stat;
|
|
44
|
+
try {
|
|
45
|
+
stat = fs.statSync(root);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue; // absent → nothing to protect
|
|
49
|
+
}
|
|
50
|
+
if ((stat.mode & GROUP_OTHER_BITS) !== 0)
|
|
51
|
+
offenders.push(`${root}/ (${modeOctal(stat.mode)}, want 700)`);
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = fs.readdirSync(root, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const abs = path.join(root, entry);
|
|
61
|
+
let entryStat;
|
|
62
|
+
try {
|
|
63
|
+
entryStat = fs.statSync(abs);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if ((entryStat.mode & GROUP_OTHER_BITS) === 0)
|
|
69
|
+
continue;
|
|
70
|
+
offenders.push(entryStat.isDirectory()
|
|
71
|
+
? `${abs}/ (${modeOctal(entryStat.mode)}, want 700)`
|
|
72
|
+
: `${abs} (${modeOctal(entryStat.mode)}, want 600)`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (offenders.length === 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
const preview = offenders.slice(0, 5).join("; ") + (offenders.length > 5 ? `; +${offenders.length - 5} more` : "");
|
|
78
|
+
return {
|
|
79
|
+
name: "secret-file-perms",
|
|
80
|
+
kind: "deterministic",
|
|
81
|
+
status: "warn",
|
|
82
|
+
confidence: "high",
|
|
83
|
+
message: `${offenders.length} env/secret/backup path(s) are readable by group/other: ${preview}. ` +
|
|
84
|
+
"Tighten with chmod 600 (files) / chmod 700 (dirs) — these hold tokens, keys, and config snapshots.",
|
|
85
|
+
evidence: { offenders: offenders.slice(0, OFFENDER_EVIDENCE_CAP) },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* `binary-config-skew` (08-F3): warn when config.json carries a configVersion
|
|
90
|
+
* NEWER than (or unorderable against) this binary's CURRENT_CONFIG_VERSION —
|
|
91
|
+
* i.e. a newer/foreign akm wrote the shared config and this install is stale.
|
|
92
|
+
* That is exactly the state where auto-migration is skipped (downgrade
|
|
93
|
+
* protection) and the proven multi-install incident class begins. Silent for
|
|
94
|
+
* same/older versions (auto-migration handles those) and unreadable configs
|
|
95
|
+
* (config loading surfaces its own errors).
|
|
96
|
+
*/
|
|
97
|
+
export function collectConfigSkewAdvisory(configPath) {
|
|
98
|
+
let raw;
|
|
99
|
+
try {
|
|
100
|
+
raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const onDisk = raw.configVersion;
|
|
106
|
+
const order = compareConfigVersion(onDisk, CURRENT_CONFIG_VERSION);
|
|
107
|
+
const skewed = order === 1 || (onDisk !== undefined && order === undefined);
|
|
108
|
+
if (!skewed)
|
|
109
|
+
return undefined;
|
|
110
|
+
return {
|
|
111
|
+
name: "binary-config-skew",
|
|
112
|
+
kind: "deterministic",
|
|
113
|
+
status: "warn",
|
|
114
|
+
confidence: "high",
|
|
115
|
+
message: `config.json has configVersion ${JSON.stringify(onDisk)} but this binary knows ${CURRENT_CONFIG_VERSION} — ` +
|
|
116
|
+
"a newer akm wrote the shared config, so this install is stale and auto-migration is skipped " +
|
|
117
|
+
"(downgrade protection). Upgrade this install; do not keep a stale binary against the shared config/DBs.",
|
|
118
|
+
evidence: { onDiskConfigVersion: onDisk, binaryConfigVersion: CURRENT_CONFIG_VERSION },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `orphan-stores` (08-F4/F7): stores that look load-bearing but are dead —
|
|
123
|
+
* legacy `config-backups/` dirs in $DATA/$CONFIG (only `$CACHE/config-backups`
|
|
124
|
+
* is written and pruned today) and a 0-byte `<stash>/.akm/state.db` decoy that
|
|
125
|
+
* misleads debugging (the live state.db lives in the data dir). Read-only:
|
|
126
|
+
* this advisory NAMES them; removal stays an owner-approved per-path action.
|
|
127
|
+
*/
|
|
128
|
+
export function collectOrphanStoresAdvisory(input) {
|
|
129
|
+
const orphans = [];
|
|
130
|
+
for (const [label, dir] of [
|
|
131
|
+
["legacy data-dir backup location", path.join(input.dataDir, "config-backups")],
|
|
132
|
+
["legacy config-dir backup location", path.join(input.configDir, "config-backups")],
|
|
133
|
+
]) {
|
|
134
|
+
try {
|
|
135
|
+
if (fs.statSync(dir).isDirectory())
|
|
136
|
+
orphans.push(`${dir}/ (${label}; live backups are in $CACHE/config-backups)`);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// absent — fine
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const decoy = path.join(input.stashDir, ".akm", "state.db");
|
|
143
|
+
try {
|
|
144
|
+
const stat = fs.statSync(decoy);
|
|
145
|
+
if (stat.isFile() && stat.size === 0) {
|
|
146
|
+
orphans.push(`${decoy} (0-byte decoy; the live state.db is in the data dir)`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// absent — fine
|
|
151
|
+
}
|
|
152
|
+
if (orphans.length === 0)
|
|
153
|
+
return undefined;
|
|
154
|
+
return {
|
|
155
|
+
name: "orphan-stores",
|
|
156
|
+
kind: "deterministic",
|
|
157
|
+
status: "warn",
|
|
158
|
+
confidence: "high",
|
|
159
|
+
message: `${orphans.length} orphan store(s) found: ${orphans.join("; ")}. ` +
|
|
160
|
+
"These are dead locations that mislead debugging/recovery — review and remove them (owner-approved, per-path).",
|
|
161
|
+
evidence: { orphans },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* `egress-endpoints` (08 surfaces 3/9): the full list of remote destinations
|
|
166
|
+
* akm can talk to under the effective config — registries, remote sources,
|
|
167
|
+
* LLM endpoints, embedding endpoint — as one pass-status informational entry
|
|
168
|
+
* for eyeball diff against expectations. Silent only when nothing remote is
|
|
169
|
+
* configured at all.
|
|
170
|
+
*/
|
|
171
|
+
export function collectEgressAdvisory(config) {
|
|
172
|
+
if (!config)
|
|
173
|
+
return undefined;
|
|
174
|
+
const endpoints = [];
|
|
175
|
+
for (const reg of config.registries ?? []) {
|
|
176
|
+
if (reg.enabled === false || !reg.url)
|
|
177
|
+
continue;
|
|
178
|
+
endpoints.push(`registry ${reg.name ?? "(unnamed)"}: ${reg.url}`);
|
|
179
|
+
}
|
|
180
|
+
for (const source of config.sources ?? []) {
|
|
181
|
+
if (source.enabled === false || !source.url)
|
|
182
|
+
continue;
|
|
183
|
+
endpoints.push(`source ${source.name ?? "(unnamed)"} (${source.type ?? "?"}): ${source.url}`);
|
|
184
|
+
}
|
|
185
|
+
for (const [name, profile] of Object.entries(config.profiles?.llm ?? {})) {
|
|
186
|
+
if (!profile?.endpoint)
|
|
187
|
+
continue;
|
|
188
|
+
endpoints.push(`llm ${name}: ${profile.endpoint}`);
|
|
189
|
+
}
|
|
190
|
+
if (config.embedding?.endpoint)
|
|
191
|
+
endpoints.push(`embedding: ${config.embedding.endpoint}`);
|
|
192
|
+
if (endpoints.length === 0)
|
|
193
|
+
return undefined;
|
|
194
|
+
return {
|
|
195
|
+
name: "egress-endpoints",
|
|
196
|
+
kind: "deterministic",
|
|
197
|
+
status: "pass",
|
|
198
|
+
confidence: "high",
|
|
199
|
+
message: `${endpoints.length} remote endpoint(s) in the effective config (registries/sources/LLM/embedding) — ` +
|
|
200
|
+
"review the evidence list for unexpected destinations.",
|
|
201
|
+
evidence: { endpoints },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Aggregate the four collectors into the advisories array shape `akmHealth`
|
|
206
|
+
* consumes. Order is fixed: perms → skew → orphans → egress.
|
|
207
|
+
*/
|
|
208
|
+
export function collectSurfacesAdvisories(input) {
|
|
209
|
+
const results = [
|
|
210
|
+
collectSecretPermsAdvisory({ stashDir: input.stashDir, cacheDir: input.cacheDir }, input.platform ?? process.platform),
|
|
211
|
+
collectConfigSkewAdvisory(input.configPath),
|
|
212
|
+
collectOrphanStoresAdvisory({ dataDir: input.dataDir, configDir: input.configDir, stashDir: input.stashDir }),
|
|
213
|
+
collectEgressAdvisory(input.config),
|
|
214
|
+
];
|
|
215
|
+
return results.filter((r) => r !== undefined);
|
|
216
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
import { queryImproveRuns } from "../../storage/repositories/improve-runs-repository.js";
|
|
5
|
+
import { queryCompletedTaskIntervals, queryTaskHistory } from "../../storage/repositories/task-history-repository.js";
|
|
6
|
+
import { projectImproveRunSummary } from "./improve-metrics.js";
|
|
7
|
+
/**
|
|
8
|
+
* Load task_history intervals for `task_id='akm-improve'` in the window.
|
|
9
|
+
* Returned sorted by startMs ascending so containment lookups can use a
|
|
10
|
+
* linear scan (typical N is ~24/day; not worth a tree).
|
|
11
|
+
*
|
|
12
|
+
* The window filter is widened by 5 minutes on each side because the cron
|
|
13
|
+
* task wraps `akm improve` — the task `started_at` fires at e.g. :07:01
|
|
14
|
+
* while `recordImproveRun` writes the matching `improve_runs.started_at`
|
|
15
|
+
* later (after config load, planning, etc.), so the improve_runs row can
|
|
16
|
+
* be inside the window even when its enclosing task_history row started
|
|
17
|
+
* just before the window opened.
|
|
18
|
+
*/
|
|
19
|
+
function loadTaskIntervals(db, since, until) {
|
|
20
|
+
const sinceMs = new Date(since).getTime();
|
|
21
|
+
const untilMs = until ? new Date(until).getTime() : Number.POSITIVE_INFINITY;
|
|
22
|
+
const widenedSince = new Date(sinceMs - 5 * 60 * 1000).toISOString();
|
|
23
|
+
const widenedUntil = Number.isFinite(untilMs) ? new Date(untilMs + 5 * 60 * 1000).toISOString() : undefined;
|
|
24
|
+
const rows = queryCompletedTaskIntervals(db, widenedSince, widenedUntil);
|
|
25
|
+
const intervals = [];
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
const startMs = new Date(row.started_at).getTime();
|
|
28
|
+
const endMs = new Date(row.completed_at).getTime();
|
|
29
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs)
|
|
30
|
+
continue;
|
|
31
|
+
intervals.push({ startMs, endMs, durationMs: endMs - startMs });
|
|
32
|
+
}
|
|
33
|
+
return intervals;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Find the task_history interval that contains the given timestamp. The
|
|
37
|
+
* task wraps `akm improve`, so `improve_runs.started_at` (when
|
|
38
|
+
* `recordImproveRun` writes) always falls inside the enclosing task's
|
|
39
|
+
* [started_at, completed_at]. Returns undefined when no interval
|
|
40
|
+
* contains the timestamp (which happens for manually-invoked improve
|
|
41
|
+
* runs not driven by the `akm-improve` task).
|
|
42
|
+
*
|
|
43
|
+
* Linear scan because N is small. We tolerate a 1s slop on the upper
|
|
44
|
+
* bound to handle clock skew between the wrapper's `completed_at` write
|
|
45
|
+
* and recordImproveRun's `started_at` write.
|
|
46
|
+
*/
|
|
47
|
+
function findContainingTaskInterval(timestampMs, intervals) {
|
|
48
|
+
const SLOP_MS = 1000;
|
|
49
|
+
for (const interval of intervals) {
|
|
50
|
+
if (timestampMs >= interval.startMs && timestampMs <= interval.endMs + SLOP_MS) {
|
|
51
|
+
return interval;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Load `task_history` rows whose `task_id` begins `akm-improve` (the scheduled
|
|
58
|
+
* improve tasks: `akm-improve-frequent`, `akm-improve-proactive-weekly`, …) in
|
|
59
|
+
* the window, widened ±5 min so a task that fired just before the window opened
|
|
60
|
+
* still matches a run inside it. Used to attribute each improve run to the task
|
|
61
|
+
* that launched it.
|
|
62
|
+
*/
|
|
63
|
+
function loadImproveTaskRuns(db, since, until) {
|
|
64
|
+
const sinceMs = new Date(since).getTime();
|
|
65
|
+
const untilMs = until ? new Date(until).getTime() : undefined;
|
|
66
|
+
const widenedSince = new Date(sinceMs - 5 * 60 * 1000).toISOString();
|
|
67
|
+
const widenedUntil = untilMs !== undefined ? new Date(untilMs + 5 * 60 * 1000).toISOString() : undefined;
|
|
68
|
+
const runs = [];
|
|
69
|
+
for (const row of queryTaskHistory(db, { since: widenedSince, until: widenedUntil })) {
|
|
70
|
+
if (!row.task_id.startsWith("akm-improve"))
|
|
71
|
+
continue;
|
|
72
|
+
const startMs = new Date(row.started_at).getTime();
|
|
73
|
+
if (!Number.isFinite(startMs))
|
|
74
|
+
continue;
|
|
75
|
+
const endIso = row.completed_at ?? row.failed_at;
|
|
76
|
+
const endMs = endIso ? new Date(endIso).getTime() : Number.NaN;
|
|
77
|
+
runs.push({ taskId: row.task_id, startMs, endMs });
|
|
78
|
+
}
|
|
79
|
+
return runs;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Attribute an improve run to the scheduled task that launched it by matching
|
|
83
|
+
* start times within ±5 min, scored by start delta (plus end delta when both
|
|
84
|
+
* ends are known). Port of the health-report skill's `match_task_id`. Returns
|
|
85
|
+
* `"manual"` when no scheduled improve task matches.
|
|
86
|
+
*/
|
|
87
|
+
export function matchImproveTaskId(startedAt, completedAt, taskRuns) {
|
|
88
|
+
const startMs = new Date(startedAt).getTime();
|
|
89
|
+
if (!Number.isFinite(startMs))
|
|
90
|
+
return "manual";
|
|
91
|
+
const endMs = completedAt ? new Date(completedAt).getTime() : Number.NaN;
|
|
92
|
+
let best;
|
|
93
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
94
|
+
for (const task of taskRuns) {
|
|
95
|
+
const startDelta = Math.abs(task.startMs - startMs);
|
|
96
|
+
if (startDelta > 5 * 60 * 1000)
|
|
97
|
+
continue;
|
|
98
|
+
let score = startDelta;
|
|
99
|
+
if (Number.isFinite(endMs) && Number.isFinite(task.endMs))
|
|
100
|
+
score += Math.abs(task.endMs - endMs);
|
|
101
|
+
if (score < bestScore) {
|
|
102
|
+
bestScore = score;
|
|
103
|
+
best = task.taskId;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return best ?? "manual";
|
|
107
|
+
}
|
|
108
|
+
export function buildPerRunSummaries(db, since, until) {
|
|
109
|
+
const rows = queryImproveRuns(db, since, until);
|
|
110
|
+
const taskIntervals = loadTaskIntervals(db, since, until);
|
|
111
|
+
const improveTaskRuns = loadImproveTaskRuns(db, since, until);
|
|
112
|
+
const summaries = [];
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
const startMs = new Date(row.started_at).getTime();
|
|
115
|
+
const endMs = new Date(row.completed_at).getTime();
|
|
116
|
+
// Prefer the improve_runs row's own (completed_at - started_at) delta:
|
|
117
|
+
// recordImproveRun now persists distinct start/end timestamps, so the
|
|
118
|
+
// row's own delta is the authoritative per-run wall time even for
|
|
119
|
+
// manually-invoked `akm improve` runs with no enclosing task_history.
|
|
120
|
+
// Only fall back to the task_history containing-interval join for legacy/
|
|
121
|
+
// backfill rows where started_at == completed_at (row delta is 0).
|
|
122
|
+
const hasRowDelta = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs;
|
|
123
|
+
let wallTimeMs;
|
|
124
|
+
if (hasRowDelta) {
|
|
125
|
+
wallTimeMs = endMs - startMs;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const interval = Number.isFinite(startMs) ? findContainingTaskInterval(startMs, taskIntervals) : undefined;
|
|
129
|
+
wallTimeMs = interval?.durationMs ?? 0;
|
|
130
|
+
}
|
|
131
|
+
const taskId = matchImproveTaskId(row.started_at, row.completed_at, improveTaskRuns);
|
|
132
|
+
summaries.push(projectImproveRunSummary(row, wallTimeMs, taskId));
|
|
133
|
+
}
|
|
134
|
+
return summaries;
|
|
135
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
* Lanes ratified as ENRICHMENT-ONLY: they may propose edits to existing
|
|
6
|
+
* assets (metadata, relations, content refresh) but must not mint new ones.
|
|
7
|
+
* New-asset generation belongs to the signal-gated minting lanes
|
|
8
|
+
* (extract/distill/memory-inference/recombine).
|
|
9
|
+
*/
|
|
10
|
+
export const ENRICHMENT_LANES = ["proactive", "high-salience", "high-retrieval", "signal-delta"];
|
|
11
|
+
/** Minted share of enrichment-lane accepts that triggers a WARN advisory. */
|
|
12
|
+
export const ENRICHMENT_MINTED_WARN_SHARE = 0.05;
|
|
13
|
+
/** Minted share of enrichment-lane accepts that triggers a FAIL advisory. */
|
|
14
|
+
export const ENRICHMENT_MINTED_FAIL_SHARE = 0.15;
|
|
15
|
+
/**
|
|
16
|
+
* Cron task failure rate at or above which the `task-fail-rate` health advisory
|
|
17
|
+
* warns. 0.05 (5%) is the SAME threshold the HTML report already applies as its
|
|
18
|
+
* fail-rate pass/warn cutoff (`failOk = taskFailRate < 0.05` in
|
|
19
|
+
* src/commands/health/html-report.ts) — this constant makes it the single
|
|
20
|
+
* source so the advisory and the rendered badge cannot drift.
|
|
21
|
+
*/
|
|
22
|
+
export const TASK_FAIL_RATE_WARN = 0.05;
|
|
23
|
+
/** Event type recorded on each completed improve run. */
|
|
24
|
+
export const IMPROVE_COMPLETED_EVENT = "improve_completed";
|
|
25
|
+
/** An active task older than this (ms) is treated as stuck. */
|
|
26
|
+
export const ACTIVE_RUN_WARN_MS = 15 * 60 * 1000;
|
|
@@ -0,0 +1,195 @@
|
|
|
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
|
+
* Window handling for `akm health`: `--window-compare` / `--windows` parsing,
|
|
6
|
+
* per-window metric assembly, log-backing partition, and delta computation.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import { UsageError } from "../../core/errors.js";
|
|
10
|
+
import { readEvents } from "../../core/events.js";
|
|
11
|
+
import { buildTaskRunId, getLoggedRunIds } from "../../core/logs-db.js";
|
|
12
|
+
import { DURATION_UNITS, parseDuration } from "../../core/time.js";
|
|
13
|
+
import { queryTaskHistory } from "../../storage/repositories/task-history-repository.js";
|
|
14
|
+
import { buildImproveSkipSummary, computeWallTimeStats, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./improve-metrics.js";
|
|
15
|
+
import { readLlmUsageAggregate } from "./llm-usage.js";
|
|
16
|
+
import { computeDegradationMetrics, computeDenominatorFixedCoverage, readCalibration } from "./metrics.js";
|
|
17
|
+
import { buildPerRunSummaries } from "./task-runs.js";
|
|
18
|
+
import { ACTIVE_RUN_WARN_MS, IMPROVE_COMPLETED_EVENT, } from "./types.js";
|
|
19
|
+
/**
|
|
20
|
+
* Parse a `--window-compare <duration>` shorthand into two adjacent windows
|
|
21
|
+
* (current, prior). Duration syntax matches {@link parseHealthSince}.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveWindowCompare(duration, now = () => Date.now()) {
|
|
24
|
+
const trimmed = duration.trim();
|
|
25
|
+
// Canonical CLI unit grammar: `m` = minutes, `M` = months. Not lower-cased,
|
|
26
|
+
// so case distinguishes the two. See core/time.ts DURATION_UNITS.
|
|
27
|
+
const ms = parseDuration(trimmed, DURATION_UNITS);
|
|
28
|
+
if (ms === null) {
|
|
29
|
+
throw new UsageError("--window-compare must be a duration like '24h', '7d', or '30m'.", "INVALID_FLAG_VALUE");
|
|
30
|
+
}
|
|
31
|
+
if (ms <= 0) {
|
|
32
|
+
throw new UsageError("--window-compare must be a positive duration.", "INVALID_FLAG_VALUE");
|
|
33
|
+
}
|
|
34
|
+
const nowMs = now();
|
|
35
|
+
const currentSince = new Date(nowMs - ms).toISOString();
|
|
36
|
+
const currentUntil = new Date(nowMs).toISOString();
|
|
37
|
+
const priorSince = new Date(nowMs - 2 * ms).toISOString();
|
|
38
|
+
const priorUntil = currentSince;
|
|
39
|
+
return [
|
|
40
|
+
{ name: "current", since: currentSince, until: currentUntil },
|
|
41
|
+
{ name: "prior", since: priorSince, until: priorUntil },
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse a single repeatable `--windows` value of the form
|
|
46
|
+
* `name=...,since=...,until=...`. All keys are optional EXCEPT name and since.
|
|
47
|
+
*/
|
|
48
|
+
export function parseWindowSpec(raw) {
|
|
49
|
+
const fields = {};
|
|
50
|
+
for (const part of raw.split(",")) {
|
|
51
|
+
const trimmed = part.trim();
|
|
52
|
+
if (!trimmed)
|
|
53
|
+
continue;
|
|
54
|
+
const eq = trimmed.indexOf("=");
|
|
55
|
+
if (eq < 0) {
|
|
56
|
+
throw new UsageError(`--windows entry must be a comma-separated list of key=value pairs: ${raw}`, "INVALID_FLAG_VALUE");
|
|
57
|
+
}
|
|
58
|
+
const key = trimmed.slice(0, eq).trim();
|
|
59
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
60
|
+
fields[key] = value;
|
|
61
|
+
}
|
|
62
|
+
if (!fields.name) {
|
|
63
|
+
throw new UsageError(`--windows entry is missing required 'name': ${raw}`, "INVALID_FLAG_VALUE");
|
|
64
|
+
}
|
|
65
|
+
if (!fields.since) {
|
|
66
|
+
throw new UsageError(`--windows entry is missing required 'since': ${raw}`, "INVALID_FLAG_VALUE");
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
name: fields.name,
|
|
70
|
+
since: fields.since,
|
|
71
|
+
...(fields.until ? { until: fields.until } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Hard-coded list of "interesting" metric paths for window-compare deltas. */
|
|
75
|
+
export const INTERESTING_DELTA_PATHS = [
|
|
76
|
+
"improve.actions.reflect.failed",
|
|
77
|
+
"improve.actions.reflect.guardRejected",
|
|
78
|
+
"improve.actions.distill.llmFailed",
|
|
79
|
+
"improve.actions.distill.queued",
|
|
80
|
+
"improve.actions.distill.deferred",
|
|
81
|
+
"improve.consolidation.promoted",
|
|
82
|
+
"improve.memoryInference.written",
|
|
83
|
+
"improve.memoryInference.yieldRate",
|
|
84
|
+
"improve.memoryInference.skippedNoFacts",
|
|
85
|
+
"improve.memoryInference.htmlErrorCount",
|
|
86
|
+
"improve.graphExtraction.cacheHitRate",
|
|
87
|
+
"improve.graphExtraction.failures",
|
|
88
|
+
"improve.graphExtraction.htmlErrors",
|
|
89
|
+
"improve.graphExtraction.nonArrayBatchFailures",
|
|
90
|
+
"improve.sessionExtraction.sessionsScanned",
|
|
91
|
+
"improve.sessionExtraction.proposalsCreated",
|
|
92
|
+
"improve.autoAccept.promoted",
|
|
93
|
+
"improve.autoAccept.validationFailed",
|
|
94
|
+
"improve.wallTime.medianMs",
|
|
95
|
+
"improve.wallTime.p95Ms",
|
|
96
|
+
];
|
|
97
|
+
export function readNumericPath(obj, path) {
|
|
98
|
+
const parts = path.split(".");
|
|
99
|
+
let cursor = obj;
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
if (typeof cursor !== "object" || cursor === null)
|
|
102
|
+
return 0;
|
|
103
|
+
cursor = cursor[part];
|
|
104
|
+
}
|
|
105
|
+
return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
|
|
106
|
+
}
|
|
107
|
+
export function computeDeltas(first, last) {
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const path of INTERESTING_DELTA_PATHS) {
|
|
110
|
+
const from = readNumericPath(first, path);
|
|
111
|
+
const to = readNumericPath(last, path);
|
|
112
|
+
if (from === 0 && to === 0)
|
|
113
|
+
continue;
|
|
114
|
+
let pctChange;
|
|
115
|
+
if (from === 0) {
|
|
116
|
+
pctChange = to === 0 ? 0 : "+inf";
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
pctChange = Number((((to - from) / from) * 100).toFixed(2));
|
|
120
|
+
}
|
|
121
|
+
out[path] = { from, to, pctChange };
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Partition task_history rows into "should have a log" (non-null log_path) and
|
|
127
|
+
* "log is actually backed". A run counts as backed when logs.db holds rows for
|
|
128
|
+
* its run_id (#579 — the DB is the primary record); rows written before logs.db
|
|
129
|
+
* existed fall back to the transitional on-disk file check. `logsDb` may be
|
|
130
|
+
* undefined when logs.db could not be opened — then only the file check runs.
|
|
131
|
+
*/
|
|
132
|
+
export function partitionLogBackedRows(taskRows, logsDb) {
|
|
133
|
+
const withLogs = taskRows.filter((row) => row.log_path !== null);
|
|
134
|
+
const loggedRunIds = logsDb
|
|
135
|
+
? getLoggedRunIds(logsDb, withLogs.map((row) => buildTaskRunId(row.task_id, row.started_at)))
|
|
136
|
+
: new Set();
|
|
137
|
+
const backed = withLogs.filter((row) => loggedRunIds.has(buildTaskRunId(row.task_id, row.started_at)) ||
|
|
138
|
+
(row.log_path !== null && fs.existsSync(row.log_path)));
|
|
139
|
+
return { withLogs, backed };
|
|
140
|
+
}
|
|
141
|
+
export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Date.now(), logsDb) {
|
|
142
|
+
const taskRows = queryTaskHistory(db, { since }).filter((row) => {
|
|
143
|
+
const startMs = new Date(row.started_at).getTime();
|
|
144
|
+
const untilMs = new Date(until).getTime();
|
|
145
|
+
return !Number.isFinite(untilMs) || startMs < untilMs;
|
|
146
|
+
});
|
|
147
|
+
const { withLogs: taskRowsWithLogs, backed: existingLogRows } = partitionLogBackedRows(taskRows, logsDb);
|
|
148
|
+
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
149
|
+
const activeRows = taskRows.filter((row) => row.status === "active");
|
|
150
|
+
const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
|
|
151
|
+
const promptRows = taskRows.filter((row) => row.target_kind === "prompt");
|
|
152
|
+
const promptFailures = promptRows.filter((row) => {
|
|
153
|
+
const detail = parseTaskMetadata(row).detail;
|
|
154
|
+
return typeof detail?.reason === "string" && detail.reason.length > 0;
|
|
155
|
+
});
|
|
156
|
+
const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
|
|
157
|
+
const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
|
|
158
|
+
const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
|
|
159
|
+
const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime()).length;
|
|
160
|
+
const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
|
|
161
|
+
const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
|
|
162
|
+
const eventsMetrics = summarizeImproveCompleted(improveCompletedEvents);
|
|
163
|
+
const { metrics: improveSummary, runCount } = summarizeImproveRuns(db, since, until);
|
|
164
|
+
improveSummary.invoked = improveInvoked;
|
|
165
|
+
improveSummary.completed = eventsMetrics.completed;
|
|
166
|
+
const skipSummary = buildImproveSkipSummary(improveSkippedEvents);
|
|
167
|
+
improveSummary.skipped = skipSummary.skipped;
|
|
168
|
+
improveSummary.skipReasons = skipSummary.skipReasons;
|
|
169
|
+
// Preserve the per-phase aggregation computed by summarizeImproveRuns and
|
|
170
|
+
// derive top-level wall times from the same improve-runs window so counts
|
|
171
|
+
// and percentiles stay aligned with per-run reporting.
|
|
172
|
+
const perRunSummaries = buildPerRunSummaries(db, since, until);
|
|
173
|
+
const wallTimes = perRunSummaries.map((run) => run.wallTimeMs).filter((ms) => Number.isFinite(ms) && ms > 0);
|
|
174
|
+
improveSummary.wallTime = computeWallTimeStats(wallTimes, improveSummary.wallTime.byPhase);
|
|
175
|
+
improveSummary.calibration = readCalibration(db, since, until);
|
|
176
|
+
// WS-5: Compute denominator-fixed coverage from the most recent run's
|
|
177
|
+
// memorySummary (totalAssets = eligible + derived — the fixed denominator).
|
|
178
|
+
const totalAssets = improveSummary.memorySummary.eligible + improveSummary.memorySummary.derived;
|
|
179
|
+
improveSummary.coverage = computeDenominatorFixedCoverage(db, totalAssets, improveSummary.memorySummary.eligible, since, until);
|
|
180
|
+
// WS-5: Compute per-run degradation metrics (corpus diversity, merge fidelity,
|
|
181
|
+
// generation distribution, oracle spot-check). Health VIEWS only.
|
|
182
|
+
const degradation = computeDegradationMetrics(db, since, until);
|
|
183
|
+
if (degradation) {
|
|
184
|
+
improveSummary.degradation = degradation;
|
|
185
|
+
}
|
|
186
|
+
const metrics = {
|
|
187
|
+
taskFailRate: roundRate(taskFailRate),
|
|
188
|
+
agentFailureRate: roundRate(agentFailureRate),
|
|
189
|
+
stuckActiveRuns,
|
|
190
|
+
logBackingRate: roundRate(logBackingRate),
|
|
191
|
+
probeRoundTripMs: null,
|
|
192
|
+
llmUsage: readLlmUsageAggregate(stateDbPath, since, until),
|
|
193
|
+
};
|
|
194
|
+
return { improve: improveSummary, metrics, runs: runCount };
|
|
195
|
+
}
|