akm-cli 0.9.0-beta.9 → 0.9.0-rc.0
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 +592 -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/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-cli.js +220 -227
- 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/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 +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- 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 +382 -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/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 +259 -769
- 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 +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -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 +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- 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 +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- 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 +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- 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/scripts/migrate-storage.js +2661 -2369
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
- 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/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 +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- 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,93 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Default minimum total score a session must reach to PASS the gate. Tuned
|
|
6
|
+
* conservatively: a single real narrative-lesson session, or a procedurally
|
|
7
|
+
* dense session, clears it — but pure read-only Q&A does not. Config-overridable
|
|
8
|
+
* via `processes.extract.triage.minScore`.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_TRIAGE_MIN_SCORE = 2;
|
|
11
|
+
// Decision / outcome / error narrative markers. Case-insensitive, word-bounded.
|
|
12
|
+
const MARKER_RE = /\b(error|failed|fix(?:ed)?|root cause|turns out|because|decided|instead|gotcha|workaround|regress(?:ed)?|broke|TIL)\b/i;
|
|
13
|
+
// Edit / commit / file-write markers in tool event text.
|
|
14
|
+
const EDIT_COMMIT_RE = /\b(Edit|Write|MultiEdit|git commit|diff)\b/i;
|
|
15
|
+
// A substantive turn is an assistant or tool turn whose text is non-trivial.
|
|
16
|
+
const SUBSTANTIVE_MIN_CHARS = 40;
|
|
17
|
+
export function scoreSessionTriage(data, minScore, options) {
|
|
18
|
+
const events = data.events;
|
|
19
|
+
const total = events.length;
|
|
20
|
+
// (a) markers — count word-bounded marker hits across all event text, capped.
|
|
21
|
+
let markerHits = 0;
|
|
22
|
+
for (const e of events) {
|
|
23
|
+
if (MARKER_RE.test(e.text))
|
|
24
|
+
markerHits += 1;
|
|
25
|
+
}
|
|
26
|
+
// Each marker-bearing event contributes 1 point, capped at 2 (a single real
|
|
27
|
+
// narrative session clears the bar on markers alone).
|
|
28
|
+
const markers = Math.min(markerHits, 2);
|
|
29
|
+
// (b) toolDensity — bounded contribution from tool-use events.
|
|
30
|
+
let toolEvents = 0;
|
|
31
|
+
for (const e of events) {
|
|
32
|
+
if (e.role === "tool")
|
|
33
|
+
toolEvents += 1;
|
|
34
|
+
}
|
|
35
|
+
// 0.25 per tool event, capped at 1.5.
|
|
36
|
+
const toolDensity = Math.min(toolEvents * 0.25, 1.5);
|
|
37
|
+
// (c) editCommit — bounded contribution from edit/write/commit markers on
|
|
38
|
+
// events (filePath present, or text matching the edit/commit regex).
|
|
39
|
+
let editCommitEvents = 0;
|
|
40
|
+
for (const e of events) {
|
|
41
|
+
if (e.filePath || EDIT_COMMIT_RE.test(e.text))
|
|
42
|
+
editCommitEvents += 1;
|
|
43
|
+
}
|
|
44
|
+
// 0.25 per edit/commit event, capped at 1.5. Together with toolDensity a
|
|
45
|
+
// procedurally dense session reaches well above the default threshold.
|
|
46
|
+
const editCommit = Math.min(editCommitEvents * 0.25, 1.5);
|
|
47
|
+
// (d) substantiveRatio — scaled fraction of non-trivial assistant+tool turns.
|
|
48
|
+
let substantive = 0;
|
|
49
|
+
for (const e of events) {
|
|
50
|
+
if ((e.role === "assistant" || e.role === "tool") && e.text.length >= SUBSTANTIVE_MIN_CHARS) {
|
|
51
|
+
substantive += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const ratio = total > 0 ? substantive / total : 0;
|
|
55
|
+
// Scale to a bounded [0,1] contribution.
|
|
56
|
+
const substantiveRatio = Math.min(ratio, 1);
|
|
57
|
+
const subscores = { markers, toolDensity, editCommit, substantiveRatio };
|
|
58
|
+
const score = markers + toolDensity + editCommit + substantiveRatio;
|
|
59
|
+
// Base gate: score must clear the floor.
|
|
60
|
+
let pass = score >= minScore;
|
|
61
|
+
// #641 procedural-aware floor (opt-in, DEFAULT OFF).
|
|
62
|
+
// When enabled, a session that clears the score gate must ALSO have at least
|
|
63
|
+
// one narrative marker (markers >= 1) OR meaningful edit/commit signal
|
|
64
|
+
// (editCommit >= 0.5). Pure read-only Q&A sessions that pass only via
|
|
65
|
+
// toolDensity + substantiveRatio are rejected as low-signal.
|
|
66
|
+
if (pass && options?.proceduralAwareFloor === true) {
|
|
67
|
+
const hasProceduralSignal = markers >= 1 || editCommit >= 0.5;
|
|
68
|
+
if (!hasProceduralSignal) {
|
|
69
|
+
pass = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
pass,
|
|
74
|
+
score,
|
|
75
|
+
subscores,
|
|
76
|
+
...(pass ? {} : { reason: "low_signal" }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the effective triage config from the extract process config. Mirrors
|
|
81
|
+
* the minContentChars / schemaSimilarity resolution style in akmExtract.
|
|
82
|
+
*
|
|
83
|
+
* Default-off: `enabled` is FALSE unless `triage.enabled === true`. `minScore`
|
|
84
|
+
* defaults to {@link DEFAULT_TRIAGE_MIN_SCORE}.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveTriageConfig(extractProcess) {
|
|
87
|
+
const triage = extractProcess?.triage;
|
|
88
|
+
const enabled = triage?.enabled === true;
|
|
89
|
+
const minScore = typeof triage?.minScore === "number" ? triage.minScore : DEFAULT_TRIAGE_MIN_SCORE;
|
|
90
|
+
// #641: default-off — only true when explicitly set to true.
|
|
91
|
+
const proceduralAwareFloor = triage?.proceduralAwareFloor === true;
|
|
92
|
+
return { enabled, minScore, proceduralAwareFloor };
|
|
93
|
+
}
|
|
@@ -1,22 +1,25 @@
|
|
|
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
|
-
import path from "node:path";
|
|
5
4
|
import { BaseLinter } from "./base-linter.js";
|
|
5
|
+
/** Valid values for the `type` field in agent frontmatter. */
|
|
6
|
+
const VALID_AGENT_TYPES = ["agent"];
|
|
6
7
|
/**
|
|
7
8
|
* Linter for `agents/` assets.
|
|
8
9
|
*
|
|
9
|
-
* Extra
|
|
10
|
+
* Extra checks beyond base:
|
|
10
11
|
* - `missing-name-or-type`: frontmatter exists but `name` or `type` field is
|
|
11
12
|
* absent. Not auto-fixable; detail includes a suggested slug.
|
|
13
|
+
* - `missing-name-or-type` (invalid value): `type` is present but not a
|
|
14
|
+
* recognised agent type value.
|
|
12
15
|
*/
|
|
13
16
|
export class AgentLinter extends BaseLinter {
|
|
14
17
|
types = ["agents"];
|
|
15
18
|
lint(ctx) {
|
|
16
19
|
const issues = this.runBaseChecks(ctx);
|
|
17
|
-
const missingFieldDetail = this
|
|
20
|
+
const missingFieldDetail = this.checkMissingNameOrType(ctx.data, ctx.frontmatter);
|
|
18
21
|
if (missingFieldDetail) {
|
|
19
|
-
const slug = this
|
|
22
|
+
const slug = this.suggestSlug(ctx.filePath);
|
|
20
23
|
issues.push({
|
|
21
24
|
file: ctx.relPath,
|
|
22
25
|
issue: "missing-name-or-type",
|
|
@@ -24,26 +27,18 @@ export class AgentLinter extends BaseLinter {
|
|
|
24
27
|
fixed: false,
|
|
25
28
|
});
|
|
26
29
|
}
|
|
30
|
+
else {
|
|
31
|
+
// Only validate the value when the field is actually present.
|
|
32
|
+
const invalidTypeDetail = this.checkInvalidTypeValue(ctx.data, VALID_AGENT_TYPES);
|
|
33
|
+
if (invalidTypeDetail) {
|
|
34
|
+
issues.push({
|
|
35
|
+
file: ctx.relPath,
|
|
36
|
+
issue: "missing-name-or-type",
|
|
37
|
+
detail: invalidTypeDetail,
|
|
38
|
+
fixed: false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
27
42
|
return issues;
|
|
28
43
|
}
|
|
29
|
-
#checkMissingNameOrType(data, frontmatterText) {
|
|
30
|
-
if (!frontmatterText)
|
|
31
|
-
return null;
|
|
32
|
-
const missingFields = [];
|
|
33
|
-
if (!("name" in data) || !data.name)
|
|
34
|
-
missingFields.push("name");
|
|
35
|
-
if (!("type" in data) || !data.type)
|
|
36
|
-
missingFields.push("type");
|
|
37
|
-
if (missingFields.length === 0)
|
|
38
|
-
return null;
|
|
39
|
-
return `missing fields: ${missingFields.join(", ")}`;
|
|
40
|
-
}
|
|
41
|
-
#suggestSlug(filePath) {
|
|
42
|
-
return path
|
|
43
|
-
.basename(filePath, ".md")
|
|
44
|
-
.toLowerCase()
|
|
45
|
-
.replace(/[^a-z0-9-]+/g, "-")
|
|
46
|
-
.replace(/-+/g, "-")
|
|
47
|
-
.replace(/^-|-$/g, "");
|
|
48
|
-
}
|
|
49
44
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
import fs from "node:fs";
|
|
40
40
|
import path from "node:path";
|
|
41
41
|
import { getAssetTypes, resolveAssetPathFromName, TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
42
|
-
import { findSafeInsertionPoint } from "./markdown-insertion.js";
|
|
42
|
+
import { findFenceRegions, findSafeInsertionPoint } from "./markdown-insertion.js";
|
|
43
43
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
44
44
|
function formatDate(d) {
|
|
45
45
|
const y = d.getFullYear();
|
|
@@ -65,34 +65,69 @@ function checkUnquotedColon(frontmatterText) {
|
|
|
65
65
|
return null;
|
|
66
66
|
}
|
|
67
67
|
function fixUnquotedColon(raw) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
const lines = raw.split(/\r?\n/);
|
|
69
|
+
if (lines[0]?.trim() !== "---")
|
|
70
|
+
return raw;
|
|
71
|
+
const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
72
|
+
if (closeIdx === -1)
|
|
73
|
+
return raw;
|
|
74
|
+
for (let i = 1; i < closeIdx; i++) {
|
|
75
|
+
const m = lines[i].match(/^(description:\s*)(.*)/);
|
|
76
|
+
if (!m)
|
|
77
|
+
continue;
|
|
78
|
+
const prefix = m[1];
|
|
79
|
+
const value = m[2].trim();
|
|
80
|
+
if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
|
|
81
|
+
(value.startsWith("'") && value.endsWith("'") && value.length >= 2))
|
|
82
|
+
continue;
|
|
83
|
+
lines[i] = `${prefix}"${value.replace(/"/g, '\\"')}"`;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return lines.join("\n");
|
|
76
87
|
}
|
|
77
88
|
function checkMissingUpdated(data, frontmatterText) {
|
|
78
89
|
return frontmatterText !== null && !("updated" in data);
|
|
79
90
|
}
|
|
80
91
|
function fixMissingUpdated(raw, mtime) {
|
|
81
92
|
const dateStr = formatDate(mtime);
|
|
82
|
-
|
|
93
|
+
const lines = raw.split(/\r?\n/);
|
|
94
|
+
if (lines[0]?.trim() !== "---")
|
|
95
|
+
return raw;
|
|
96
|
+
const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
97
|
+
if (closeIdx === -1)
|
|
98
|
+
return raw;
|
|
99
|
+
lines.splice(closeIdx, 0, `updated: ${dateStr}`);
|
|
100
|
+
return lines.join("\n");
|
|
83
101
|
}
|
|
84
102
|
// ── stale-path helpers ────────────────────────────────────────────────────────
|
|
85
103
|
function checkStalePath(body) {
|
|
86
|
-
const pathRe =
|
|
104
|
+
const pathRe = /(?:\/home\/|\/tmp\/|\/var\/|\/root\/|\/opt\/)[^\s"'`)\]>,\n]+/g;
|
|
87
105
|
let match;
|
|
106
|
+
const stale = [];
|
|
88
107
|
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop
|
|
89
108
|
while ((match = pathRe.exec(body)) !== null) {
|
|
90
109
|
const candidate = match[0];
|
|
91
110
|
if (!fs.existsSync(candidate)) {
|
|
92
|
-
|
|
111
|
+
stale.push(candidate);
|
|
93
112
|
}
|
|
94
113
|
}
|
|
95
|
-
return
|
|
114
|
+
return stale;
|
|
115
|
+
}
|
|
116
|
+
// ── fence-strip helper ────────────────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Returns `body` with all fenced code block lines replaced by empty strings,
|
|
119
|
+
* so that ref-shaped tokens inside ``` examples are not flagged as broken refs.
|
|
120
|
+
*/
|
|
121
|
+
function stripFencedBlocks(body) {
|
|
122
|
+
const lines = body.split(/\r?\n/);
|
|
123
|
+
const regions = findFenceRegions(lines);
|
|
124
|
+
if (regions.length === 0)
|
|
125
|
+
return body;
|
|
126
|
+
for (const { start, end } of regions) {
|
|
127
|
+
for (let i = start; i <= end; i++)
|
|
128
|
+
lines[i] = "";
|
|
129
|
+
}
|
|
130
|
+
return lines.join("\n");
|
|
96
131
|
}
|
|
97
132
|
// ── missing-ref helpers ───────────────────────────────────────────────────────
|
|
98
133
|
/**
|
|
@@ -122,9 +157,8 @@ const REF_RE = new RegExp(`(?:^|[\\s\`"'(])((${buildRefTypeAlternation()}):[^\\s
|
|
|
122
157
|
* preserved to keep pre-0.9 behaviour byte-identical:
|
|
123
158
|
* - `script`: returns null (scripts live in nested dirs with arbitrary
|
|
124
159
|
* extensions — unresolvable by the slug-based walker, as the contract pins).
|
|
125
|
-
* - `task`:
|
|
126
|
-
*
|
|
127
|
-
* behaviour is held constant here (non-env/secret behaviour is unchanged).
|
|
160
|
+
* - `task`: M1 fix — tasks are stored as `<id>.yml` on disk, so resolve
|
|
161
|
+
* `task:` refs against `tasks/<id>.yml` to match actual on-disk layout.
|
|
128
162
|
*
|
|
129
163
|
* Exported for contract testing — see header CONTRACT block.
|
|
130
164
|
*/
|
|
@@ -132,9 +166,9 @@ export function refToRelPath(refType, refName) {
|
|
|
132
166
|
// script is intentionally unresolvable (contract-pinned).
|
|
133
167
|
if (refType === "script")
|
|
134
168
|
return null;
|
|
135
|
-
//
|
|
169
|
+
// M1: tasks are stored as .yml on disk; resolve task: refs against tasks/<id>.yml.
|
|
136
170
|
if (refType === "task")
|
|
137
|
-
return path.join(TYPE_DIRS.task ?? "tasks", `${refName}.
|
|
171
|
+
return path.join(TYPE_DIRS.task ?? "tasks", `${refName}.yml`);
|
|
138
172
|
const typeDir = TYPE_DIRS[refType];
|
|
139
173
|
if (!typeDir)
|
|
140
174
|
return null; // unknown type — skip
|
|
@@ -169,9 +203,11 @@ export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
|
|
|
169
203
|
try {
|
|
170
204
|
const knowledgeDir = path.join(root, "knowledge");
|
|
171
205
|
if (fs.existsSync(knowledgeDir) && fs.statSync(knowledgeDir).isDirectory()) {
|
|
172
|
-
const entries = fs.readdirSync(knowledgeDir);
|
|
206
|
+
const entries = fs.readdirSync(knowledgeDir, { withFileTypes: true });
|
|
173
207
|
for (const entry of entries) {
|
|
174
|
-
|
|
208
|
+
if (!entry.isDirectory())
|
|
209
|
+
continue;
|
|
210
|
+
const subPath = path.join(knowledgeDir, entry.name, `${refName}.md`);
|
|
175
211
|
if (fs.existsSync(subPath))
|
|
176
212
|
return true;
|
|
177
213
|
}
|
|
@@ -207,8 +243,10 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
|
|
|
207
243
|
const missing = [];
|
|
208
244
|
let match;
|
|
209
245
|
const re = new RegExp(REF_RE.source, REF_RE.flags);
|
|
246
|
+
// C1: Strip fenced code blocks so example refs inside ``` are not flagged.
|
|
247
|
+
const scanBody = stripFencedBlocks(body);
|
|
210
248
|
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop
|
|
211
|
-
while ((match = re.exec(
|
|
249
|
+
while ((match = re.exec(scanBody)) !== null) {
|
|
212
250
|
const fullRef = match[1]; // e.g. "workflow:foo" or "local//workflow:foo"
|
|
213
251
|
// Skip shell variables: memory:$(cmd) or knowledge:${VAR}
|
|
214
252
|
if (fullRef.includes("$(") || fullRef.includes("${")) {
|
|
@@ -241,6 +279,10 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
|
|
|
241
279
|
if (refName.length <= 1 || refName === "**") {
|
|
242
280
|
continue;
|
|
243
281
|
}
|
|
282
|
+
// Skip template placeholder refs like skill:<name> or workflow:<my-workflow>
|
|
283
|
+
if (refName.startsWith("<") || refName.includes("<")) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
244
286
|
const relPath = refToRelPath(refType, refName);
|
|
245
287
|
if (relPath === null)
|
|
246
288
|
continue; // type is skipped
|
|
@@ -279,7 +321,15 @@ function extractFrontmatterRefs(data, body) {
|
|
|
279
321
|
const fromInner = readRefsArray(innerData.refs);
|
|
280
322
|
if (fromInner !== null)
|
|
281
323
|
return fromInner;
|
|
324
|
+
// Session-checkpoint bodies are raw transcripts; ref-shaped tokens in the
|
|
325
|
+
// body are literals (grep patterns, JSON, tool output), not live refs.
|
|
326
|
+
// Return [] so missing-ref skips the body scan entirely.
|
|
327
|
+
if (typeof innerData.akm_memory_kind === "string")
|
|
328
|
+
return [];
|
|
282
329
|
}
|
|
330
|
+
// Same guard for outer frontmatter (e.g. opencode session files).
|
|
331
|
+
if (typeof data.akm_memory_kind === "string")
|
|
332
|
+
return [];
|
|
283
333
|
return null;
|
|
284
334
|
}
|
|
285
335
|
function readRefsArray(value) {
|
|
@@ -368,6 +418,47 @@ function parseInnerFrontmatterBlock(body) {
|
|
|
368
418
|
* (in practice the base class updates `ctx.raw` in place when `fix` is true).
|
|
369
419
|
*/
|
|
370
420
|
export class BaseLinter {
|
|
421
|
+
/**
|
|
422
|
+
* Check for missing `name` or `type` fields in frontmatter.
|
|
423
|
+
*
|
|
424
|
+
* Returns a detail string if fields are absent/empty, `null` if all present.
|
|
425
|
+
*/
|
|
426
|
+
checkMissingNameOrType(data, frontmatterText) {
|
|
427
|
+
if (!frontmatterText)
|
|
428
|
+
return null;
|
|
429
|
+
const missingFields = [];
|
|
430
|
+
if (!("name" in data) || !data.name)
|
|
431
|
+
missingFields.push("name");
|
|
432
|
+
if (!("type" in data) || !data.type)
|
|
433
|
+
missingFields.push("type");
|
|
434
|
+
if (missingFields.length === 0)
|
|
435
|
+
return null;
|
|
436
|
+
return `missing fields: ${missingFields.join(", ")}`;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Validate that the `type` field value is one of an allowed set.
|
|
440
|
+
*
|
|
441
|
+
* Returns a detail string if the value is present but invalid, `null` if valid or absent.
|
|
442
|
+
*/
|
|
443
|
+
checkInvalidTypeValue(data, allowedTypes) {
|
|
444
|
+
if (!("type" in data) || !data.type)
|
|
445
|
+
return null; // absent — covered by checkMissingNameOrType
|
|
446
|
+
const value = String(data.type);
|
|
447
|
+
if (allowedTypes.includes(value))
|
|
448
|
+
return null;
|
|
449
|
+
return `type field has invalid value '${value}'; expected one of: ${allowedTypes.join(", ")}`;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Derive a URL-safe slug from a file path.
|
|
453
|
+
*/
|
|
454
|
+
suggestSlug(filePath) {
|
|
455
|
+
return path
|
|
456
|
+
.basename(filePath, ".md")
|
|
457
|
+
.toLowerCase()
|
|
458
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
459
|
+
.replace(/-+/g, "-")
|
|
460
|
+
.replace(/^-|-$/g, "");
|
|
461
|
+
}
|
|
371
462
|
/**
|
|
372
463
|
* Insert one or more lines into a markdown body at a safe location.
|
|
373
464
|
*
|
|
@@ -401,30 +492,41 @@ export class BaseLinter {
|
|
|
401
492
|
const issues = [];
|
|
402
493
|
let currentRaw = ctx.raw;
|
|
403
494
|
let modified = false;
|
|
495
|
+
// M8: Parse lint_skip from frontmatter for per-file rule suppression.
|
|
496
|
+
// Accept both an array (`lint_skip: [missing-ref, stale-path]`) and a
|
|
497
|
+
// single scalar (`lint_skip: missing-ref`). Non-string entries are coerced
|
|
498
|
+
// and trimmed so loosely-typed YAML still gates correctly.
|
|
499
|
+
const rawLintSkip = ctx.data?.lint_skip;
|
|
500
|
+
const lintSkip = (Array.isArray(rawLintSkip) ? rawLintSkip : rawLintSkip != null ? [rawLintSkip] : [])
|
|
501
|
+
.map((v) => String(v).trim())
|
|
502
|
+
.filter(Boolean);
|
|
503
|
+
const shouldRun = (issueType) => !lintSkip.includes(issueType);
|
|
404
504
|
// ── 1. unquoted-colon ──────────────────────────────────────────────────
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
505
|
+
if (shouldRun("unquoted-colon")) {
|
|
506
|
+
const unquotedColonDetail = checkUnquotedColon(ctx.frontmatter);
|
|
507
|
+
if (unquotedColonDetail) {
|
|
508
|
+
if (ctx.fix) {
|
|
509
|
+
currentRaw = fixUnquotedColon(currentRaw);
|
|
510
|
+
modified = true;
|
|
511
|
+
issues.push({
|
|
512
|
+
file: ctx.relPath,
|
|
513
|
+
issue: "unquoted-colon",
|
|
514
|
+
detail: unquotedColonDetail,
|
|
515
|
+
fixed: true,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
issues.push({
|
|
520
|
+
file: ctx.relPath,
|
|
521
|
+
issue: "unquoted-colon",
|
|
522
|
+
detail: unquotedColonDetail,
|
|
523
|
+
fixed: false,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
424
526
|
}
|
|
425
|
-
}
|
|
527
|
+
} // end shouldRun("unquoted-colon")
|
|
426
528
|
// ── 2. missing-updated ─────────────────────────────────────────────────
|
|
427
|
-
if (checkMissingUpdated(ctx.data, ctx.frontmatter)) {
|
|
529
|
+
if (shouldRun("missing-updated") && checkMissingUpdated(ctx.data, ctx.frontmatter)) {
|
|
428
530
|
if (ctx.fix) {
|
|
429
531
|
let mtime;
|
|
430
532
|
try {
|
|
@@ -457,14 +559,23 @@ export class BaseLinter {
|
|
|
457
559
|
ctx.raw = currentRaw;
|
|
458
560
|
}
|
|
459
561
|
// ── 3. stale-path ──────────────────────────────────────────────────────
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
562
|
+
// M3: checkStalePath returns all stale matches; push one issue per path.
|
|
563
|
+
// M4: Also scan ctx.frontmatter for stale paths (absolute paths in frontmatter).
|
|
564
|
+
if (shouldRun("stale-path")) {
|
|
565
|
+
const staleInBody = checkStalePath(ctx.body);
|
|
566
|
+
const staleInFrontmatter = ctx.frontmatter ? checkStalePath(ctx.frontmatter) : [];
|
|
567
|
+
for (const candidate of [...staleInBody, ...staleInFrontmatter]) {
|
|
568
|
+
// M4: Suggest portable replacement when path is under stashRoot.
|
|
569
|
+
const portableHint = candidate.startsWith(ctx.stashRoot)
|
|
570
|
+
? ` (portable form: $AKM_STASH_DIR${candidate.slice(ctx.stashRoot.length)})`
|
|
571
|
+
: "";
|
|
572
|
+
issues.push({
|
|
573
|
+
file: ctx.relPath,
|
|
574
|
+
issue: "stale-path",
|
|
575
|
+
detail: `nonexistent path: ${candidate}${portableHint}`,
|
|
576
|
+
fixed: false,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
468
579
|
}
|
|
469
580
|
// ── 4. missing-ref ─────────────────────────────────────────────────────
|
|
470
581
|
// Carve-out for assets that declare an explicit `refs:` array in
|
|
@@ -481,16 +592,18 @@ export class BaseLinter {
|
|
|
481
592
|
// still run `checkMissingRefs` against the array itself to catch
|
|
482
593
|
// refs that were valid at capture time but later removed from the
|
|
483
594
|
// stash.
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
595
|
+
if (shouldRun("missing-ref")) {
|
|
596
|
+
const explicitRefs = extractFrontmatterRefs(ctx.data, ctx.body);
|
|
597
|
+
const refSource = explicitRefs !== null ? explicitRefs.join("\n") : ctx.body;
|
|
598
|
+
const missingRefs = checkMissingRefs(refSource, ctx.stashRoot, ctx.extraStashRoots);
|
|
599
|
+
for (const { ref, resolvedRelPath } of missingRefs) {
|
|
600
|
+
issues.push({
|
|
601
|
+
file: ctx.relPath,
|
|
602
|
+
issue: "missing-ref",
|
|
603
|
+
detail: `missing ref: ${ref} (resolved to ${resolvedRelPath})`,
|
|
604
|
+
fixed: false,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
494
607
|
}
|
|
495
608
|
return issues;
|
|
496
609
|
}
|
|
@@ -1,22 +1,25 @@
|
|
|
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
|
-
import path from "node:path";
|
|
5
4
|
import { BaseLinter } from "./base-linter.js";
|
|
5
|
+
/** Valid values for the `type` field in command frontmatter. */
|
|
6
|
+
const VALID_COMMAND_TYPES = ["command"];
|
|
6
7
|
/**
|
|
7
8
|
* Linter for `commands/` assets.
|
|
8
9
|
*
|
|
9
|
-
* Extra
|
|
10
|
+
* Extra checks beyond base:
|
|
10
11
|
* - `missing-name-or-type`: frontmatter exists but `name` or `type` field is
|
|
11
12
|
* absent. Not auto-fixable; detail includes a suggested slug.
|
|
13
|
+
* - `missing-name-or-type` (invalid value): `type` is present but not a
|
|
14
|
+
* recognised command type value.
|
|
12
15
|
*/
|
|
13
16
|
export class CommandLinter extends BaseLinter {
|
|
14
17
|
types = ["commands"];
|
|
15
18
|
lint(ctx) {
|
|
16
19
|
const issues = this.runBaseChecks(ctx);
|
|
17
|
-
const missingFieldDetail = this
|
|
20
|
+
const missingFieldDetail = this.checkMissingNameOrType(ctx.data, ctx.frontmatter);
|
|
18
21
|
if (missingFieldDetail) {
|
|
19
|
-
const slug = this
|
|
22
|
+
const slug = this.suggestSlug(ctx.filePath);
|
|
20
23
|
issues.push({
|
|
21
24
|
file: ctx.relPath,
|
|
22
25
|
issue: "missing-name-or-type",
|
|
@@ -24,26 +27,18 @@ export class CommandLinter extends BaseLinter {
|
|
|
24
27
|
fixed: false,
|
|
25
28
|
});
|
|
26
29
|
}
|
|
30
|
+
else {
|
|
31
|
+
// Only validate the value when the field is actually present.
|
|
32
|
+
const invalidTypeDetail = this.checkInvalidTypeValue(ctx.data, VALID_COMMAND_TYPES);
|
|
33
|
+
if (invalidTypeDetail) {
|
|
34
|
+
issues.push({
|
|
35
|
+
file: ctx.relPath,
|
|
36
|
+
issue: "missing-name-or-type",
|
|
37
|
+
detail: invalidTypeDetail,
|
|
38
|
+
fixed: false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
27
42
|
return issues;
|
|
28
43
|
}
|
|
29
|
-
#checkMissingNameOrType(data, frontmatterText) {
|
|
30
|
-
if (!frontmatterText)
|
|
31
|
-
return null;
|
|
32
|
-
const missingFields = [];
|
|
33
|
-
if (!("name" in data) || !data.name)
|
|
34
|
-
missingFields.push("name");
|
|
35
|
-
if (!("type" in data) || !data.type)
|
|
36
|
-
missingFields.push("type");
|
|
37
|
-
if (missingFields.length === 0)
|
|
38
|
-
return null;
|
|
39
|
-
return `missing fields: ${missingFields.join(", ")}`;
|
|
40
|
-
}
|
|
41
|
-
#suggestSlug(filePath) {
|
|
42
|
-
return path
|
|
43
|
-
.basename(filePath, ".md")
|
|
44
|
-
.toLowerCase()
|
|
45
|
-
.replace(/[^a-z0-9-]+/g, "-")
|
|
46
|
-
.replace(/-+/g, "-")
|
|
47
|
-
.replace(/^-|-$/g, "");
|
|
48
|
-
}
|
|
49
44
|
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* where the operator legitimately wants to set their editor — accept the
|
|
25
25
|
* FP and bypass with `--allow-insecure` after review.
|
|
26
26
|
*/
|
|
27
|
+
import fs from "node:fs";
|
|
27
28
|
import { listKeys } from "../env/env.js";
|
|
28
29
|
// ── Dangerous key set ─────────────────────────────────────────────────────────
|
|
29
30
|
export const DANGEROUS_VAULT_KEYS = new Set([
|
|
@@ -95,6 +96,10 @@ export const DANGEROUS_VAULT_KEY_PATTERNS = [
|
|
|
95
96
|
pattern: /^BASH_FUNC_/,
|
|
96
97
|
reason: "Shellshock-class bash function injection (CVE-2014-6271)",
|
|
97
98
|
},
|
|
99
|
+
{
|
|
100
|
+
pattern: /^GIT_CONFIG_/,
|
|
101
|
+
reason: "Git config injection through environment override variables",
|
|
102
|
+
},
|
|
98
103
|
];
|
|
99
104
|
/**
|
|
100
105
|
* Returns `true` if the given key name is dangerous — either by literal match
|
|
@@ -122,16 +127,48 @@ export function isDangerousVaultKey(key) {
|
|
|
122
127
|
* @param vaultRef Human-readable vault ref (e.g. `"vault:prod"`) shown in
|
|
123
128
|
* the finding message.
|
|
124
129
|
*/
|
|
130
|
+
/** Suppression comment token checked case-insensitively on the preceding non-empty line. */
|
|
131
|
+
const SUPPRESSION_COMMENT = "# akm-lint-ok: dangerous-vault-key";
|
|
132
|
+
/**
|
|
133
|
+
* Returns the set of keys suppressed by an inline `# akm-lint-ok: dangerous-vault-key`
|
|
134
|
+
* comment on the line immediately preceding the key assignment in the `.env` file.
|
|
135
|
+
*/
|
|
136
|
+
function collectSuppressedKeys(vaultPath) {
|
|
137
|
+
const suppressed = new Set();
|
|
138
|
+
let raw;
|
|
139
|
+
try {
|
|
140
|
+
raw = fs.readFileSync(vaultPath, "utf8");
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return suppressed;
|
|
144
|
+
}
|
|
145
|
+
const lines = raw.split(/\r?\n/);
|
|
146
|
+
let prevNonEmpty = "";
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
const trimmed = line.trim();
|
|
149
|
+
if (trimmed === "")
|
|
150
|
+
continue;
|
|
151
|
+
const keyMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
152
|
+
if (keyMatch && prevNonEmpty.toLowerCase() === SUPPRESSION_COMMENT) {
|
|
153
|
+
suppressed.add(keyMatch[1]);
|
|
154
|
+
}
|
|
155
|
+
prevNonEmpty = trimmed;
|
|
156
|
+
}
|
|
157
|
+
return suppressed;
|
|
158
|
+
}
|
|
125
159
|
export function checkVaultForDangerousKeys(vaultPath, relPath, vaultRef) {
|
|
126
160
|
const { keys } = listKeys(vaultPath);
|
|
161
|
+
const suppressed = collectSuppressedKeys(vaultPath);
|
|
127
162
|
const issues = [];
|
|
128
163
|
for (const key of keys) {
|
|
129
164
|
if (!isDangerousVaultKey(key))
|
|
130
165
|
continue;
|
|
166
|
+
if (suppressed.has(key))
|
|
167
|
+
continue;
|
|
131
168
|
issues.push({
|
|
132
169
|
file: relPath,
|
|
133
170
|
issue: "dangerous-vault-key",
|
|
134
|
-
detail: `Env key \`${key}\` can be used to hijack process execution when injected via \`akm env run\`. Ref: ${vaultRef}. Review this file before running \`akm env run\` commands against untrusted stashes
|
|
171
|
+
detail: `Env key \`${key}\` can be used to hijack process execution when injected via \`akm env run\`. Ref: ${vaultRef}. Review this file before running \`akm env run\` commands against untrusted stashes. (suppress with: ${SUPPRESSION_COMMENT} on previous line)`,
|
|
135
172
|
fixed: false,
|
|
136
173
|
});
|
|
137
174
|
}
|