akm-cli 0.9.0-beta.5 → 0.9.0-beta.51
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 +711 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +6 -2
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +38 -10
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +27 -11
- package/dist/commands/agent/agent-dispatch.js +2 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +17 -4
- package/dist/commands/config-cli.js +18 -2
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +33 -26
- package/dist/commands/env/secret-cli.js +36 -22
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/checks.js +49 -1
- package/dist/commands/health/html-report.js +422 -80
- package/dist/commands/health.js +386 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +81 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +635 -660
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +159 -69
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +124 -2
- package/dist/commands/improve/extract-prompt.js +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +389 -40
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +138 -7
- package/dist/commands/improve/improve-cli.js +36 -61
- package/dist/commands/improve/improve-profiles.js +14 -0
- package/dist/commands/improve/improve-result-file.js +14 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2498
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1083 -0
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/preparation.js +1966 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +850 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +183 -40
- package/dist/commands/improve/salience.js +438 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +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/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +43 -50
- package/dist/commands/proposal/proposal-cli.js +21 -31
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +7 -2
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +189 -63
- package/dist/commands/read/curate.js +414 -94
- package/dist/commands/read/knowledge.js +6 -3
- package/dist/commands/read/search-cli.js +9 -4
- package/dist/commands/read/search.js +10 -6
- package/dist/commands/read/show.js +86 -7
- package/dist/commands/sources/init.js +49 -17
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +43 -45
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/stash-cli.js +28 -40
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/commands/tasks/tasks-cli.js +19 -27
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +21 -35
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +18 -2
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +0 -5
- package/dist/core/config/config-migration.js +12 -11
- package/dist/core/config/config-schema.js +340 -56
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +28 -7
- package/dist/core/events.js +3 -7
- package/dist/core/improve-types.js +11 -8
- package/dist/core/logs-db.js +10 -66
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +3 -0
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +714 -0
- package/dist/core/state-db.js +525 -474
- package/dist/indexer/db/db.js +439 -247
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +218 -4
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +123 -221
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +19 -6
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/search/search-source.js +17 -18
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/config.js +6 -53
- package/dist/integrations/agent/index.js +2 -18
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +45 -15
- package/dist/llm/embedder.js +42 -3
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-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 +74 -2
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/text/helpers.js +16 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +22 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2617 -1961
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
- package/dist/setup/setup.js +29 -8
- 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/tar-utils.js +16 -8
- 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/registry-cache.js +92 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +5 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +1 -117
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +3 -2
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +13 -11
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
|
@@ -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
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { BaseLinter } from "./base-linter.js";
|
|
5
|
+
/** Recommended `category` values for facts (see docs/design/fact-asset-type.md). */
|
|
6
|
+
const KNOWN_CATEGORIES = new Set(["personal", "team", "project", "convention", "meta"]);
|
|
7
|
+
/**
|
|
8
|
+
* Linter for `facts/` assets.
|
|
9
|
+
*
|
|
10
|
+
* Extra check beyond base:
|
|
11
|
+
* - `missing-category`: a fact without a non-empty `category` frontmatter
|
|
12
|
+
* key. Category scopes the fact (personal/team/project/convention/meta)
|
|
13
|
+
* and drives how it is surfaced/injected, so it is expected. Reported as a
|
|
14
|
+
* non-fixable warning — we cannot infer the right scope automatically.
|
|
15
|
+
*/
|
|
16
|
+
export class FactLinter extends BaseLinter {
|
|
17
|
+
types = ["facts"];
|
|
18
|
+
lint(ctx) {
|
|
19
|
+
const issues = this.runBaseChecks(ctx);
|
|
20
|
+
const category = typeof ctx.data.category === "string" ? ctx.data.category.trim() : "";
|
|
21
|
+
if (!category) {
|
|
22
|
+
issues.push({
|
|
23
|
+
file: ctx.relPath,
|
|
24
|
+
issue: "missing-category",
|
|
25
|
+
detail: "fact is missing a `category` (personal|team|project|convention|meta)",
|
|
26
|
+
fixed: false,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else if (!KNOWN_CATEGORIES.has(category)) {
|
|
30
|
+
issues.push({
|
|
31
|
+
file: ctx.relPath,
|
|
32
|
+
issue: "missing-category",
|
|
33
|
+
detail: `unrecognized category "${category}" (expected one of: ${[...KNOWN_CATEGORIES].join(", ")})`,
|
|
34
|
+
fixed: false,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return issues;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -20,6 +20,7 @@ const STASH_SUBDIRS = [
|
|
|
20
20
|
"lessons",
|
|
21
21
|
"tasks",
|
|
22
22
|
"knowledge",
|
|
23
|
+
"facts",
|
|
23
24
|
];
|
|
24
25
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
25
26
|
function collectYamlFiles(dir) {
|
|
@@ -85,7 +86,8 @@ export function akmLint(options = {}) {
|
|
|
85
86
|
const fix = options.fix ?? false;
|
|
86
87
|
const fixed = [];
|
|
87
88
|
const flagged = [];
|
|
88
|
-
|
|
89
|
+
const dirsToScan = options.typeFilter ? STASH_SUBDIRS.filter((d) => d === options.typeFilter) : STASH_SUBDIRS;
|
|
90
|
+
for (const subdir of dirsToScan) {
|
|
89
91
|
const dirPath = path.join(stashRoot, subdir);
|
|
90
92
|
// Tasks are .yml files; everything else is .md
|
|
91
93
|
const files = subdir === "tasks" ? collectYamlFiles(dirPath) : collectMarkdownFiles(dirPath);
|
|
@@ -97,7 +99,9 @@ export function akmLint(options = {}) {
|
|
|
97
99
|
if (entry.isDirectory()) {
|
|
98
100
|
const subdirIssues = linter.lintDirectory(path.join(dirPath, entry.name), stashRoot);
|
|
99
101
|
for (const issue of subdirIssues) {
|
|
100
|
-
|
|
102
|
+
// Tristate-safe: only `true` counts as fixed; `false` and "failed"
|
|
103
|
+
// are both flagged.
|
|
104
|
+
if (issue.fixed === true) {
|
|
101
105
|
fixed.push(issue);
|
|
102
106
|
}
|
|
103
107
|
else {
|
|
@@ -108,6 +112,9 @@ export function akmLint(options = {}) {
|
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
for (const filePath of files) {
|
|
115
|
+
// Skip registry-cached read-only files — --fix must not mutate them.
|
|
116
|
+
if (filePath.includes("/.cache/") || filePath.includes("/registry/"))
|
|
117
|
+
continue;
|
|
111
118
|
const relPath = path.relative(stashRoot, filePath);
|
|
112
119
|
let raw;
|
|
113
120
|
try {
|
|
@@ -142,10 +149,11 @@ export function akmLint(options = {}) {
|
|
|
142
149
|
fileDeleted = true;
|
|
143
150
|
fixed.push(issue);
|
|
144
151
|
}
|
|
145
|
-
else if (issue.fixed) {
|
|
152
|
+
else if (issue.fixed === true) {
|
|
146
153
|
fixed.push(issue);
|
|
147
154
|
}
|
|
148
155
|
else {
|
|
156
|
+
// fixed === false (not fixable / no fix requested) or "failed" (fix attempted but threw)
|
|
149
157
|
flagged.push(issue);
|
|
150
158
|
}
|
|
151
159
|
}
|
|
@@ -159,16 +167,26 @@ export function akmLint(options = {}) {
|
|
|
159
167
|
// findings go into `flagged`, never `fixed`.
|
|
160
168
|
const envRoots = [stashRoot, ...extraStashRoots];
|
|
161
169
|
for (const root of envRoots) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
// The `env` assets live under `env/` (ref prefix `env:`); whole-file
|
|
171
|
+
// `secret` assets live under `secrets/` (canonical ref prefix `secret:`,
|
|
172
|
+
// singular). Map the scan directory to its canonical ref prefix so the
|
|
173
|
+
// finding's `Ref:` field matches what `akm show`/`akm secret` accept.
|
|
174
|
+
for (const { scanSubdir, refPrefix } of [
|
|
175
|
+
{ scanSubdir: "env", refPrefix: "env" },
|
|
176
|
+
{ scanSubdir: "secrets", refPrefix: "secret" },
|
|
177
|
+
]) {
|
|
178
|
+
const dir = path.join(root, scanSubdir);
|
|
179
|
+
if (!fs.existsSync(dir))
|
|
180
|
+
continue;
|
|
181
|
+
for (const envPath of collectEnvFiles(dir)) {
|
|
182
|
+
const baseName = path.basename(envPath, ".env");
|
|
183
|
+
// A dotfile literally named `.env` has an empty baseName — use the full
|
|
184
|
+
// basename so it doesn't collide with `default.env` → refPrefix:default.
|
|
185
|
+
const ref = baseName === "" ? `${refPrefix}:.env` : `${refPrefix}:${baseName}`;
|
|
186
|
+
const relPath = path.relative(root, envPath);
|
|
187
|
+
for (const issue of checkEnvForDangerousKeys(envPath, relPath, ref)) {
|
|
188
|
+
flagged.push(issue);
|
|
189
|
+
}
|
|
172
190
|
}
|
|
173
191
|
}
|
|
174
192
|
}
|
|
@@ -33,7 +33,7 @@ export class MemoryLinter extends BaseLinter {
|
|
|
33
33
|
file: ctx.relPath,
|
|
34
34
|
issue: "orphaned-stub",
|
|
35
35
|
detail: `could not delete: ${e instanceof Error ? e.message : String(e)}`,
|
|
36
|
-
fixed:
|
|
36
|
+
fixed: "failed",
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
// Signal caller to skip remaining checks via a sentinel issue
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { AgentLinter } from "./agent-linter.js";
|
|
5
5
|
import { CommandLinter } from "./command-linter.js";
|
|
6
6
|
import { DefaultLinter } from "./default-linter.js";
|
|
7
|
+
import { FactLinter } from "./fact-linter.js";
|
|
7
8
|
import { KnowledgeLinter } from "./knowledge-linter.js";
|
|
8
9
|
import { MemoryLinter } from "./memory-linter.js";
|
|
9
10
|
import { SkillLinter } from "./skill-linter.js";
|
|
@@ -18,15 +19,19 @@ const LINTERS = [
|
|
|
18
19
|
new KnowledgeLinter(),
|
|
19
20
|
new SkillLinter(),
|
|
20
21
|
new TaskLinter(),
|
|
21
|
-
new
|
|
22
|
+
new FactLinter(),
|
|
22
23
|
];
|
|
24
|
+
// Single shared DefaultLinter instance — used both as the explicit "lessons"
|
|
25
|
+
// handler and as the fallback for any unrecognised asset type.
|
|
26
|
+
const DEFAULT_LINTER = new DefaultLinter();
|
|
23
27
|
const LINTER_MAP = new Map();
|
|
24
28
|
for (const linter of LINTERS) {
|
|
25
29
|
for (const t of linter.types) {
|
|
26
30
|
LINTER_MAP.set(t, linter);
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
|
-
|
|
33
|
+
// Register "lessons" explicitly so there is only one DefaultLinter instance.
|
|
34
|
+
LINTER_MAP.set("lessons", DEFAULT_LINTER);
|
|
30
35
|
/**
|
|
31
36
|
* Return the appropriate linter for the given stash subdirectory name.
|
|
32
37
|
* Falls back to `DefaultLinter` for unknown types.
|
|
@@ -28,9 +28,9 @@ export class TaskLinter extends BaseLinter {
|
|
|
28
28
|
if (!("schedule" in ctx.data) || typeof ctx.data.schedule !== "string" || ctx.data.schedule.trim() === "") {
|
|
29
29
|
missing.push("schedule");
|
|
30
30
|
}
|
|
31
|
-
// enabled: must be present
|
|
32
|
-
if (!("enabled" in ctx.data)) {
|
|
33
|
-
missing.push("enabled");
|
|
31
|
+
// enabled: must be present AND a boolean (value of false is valid, but "yes" / 1 are not)
|
|
32
|
+
if (!("enabled" in ctx.data) || typeof ctx.data.enabled !== "boolean") {
|
|
33
|
+
missing.push("enabled (must be a boolean)");
|
|
34
34
|
}
|
|
35
35
|
// At least one of: prompt, workflow, or command
|
|
36
36
|
const hasTarget = "prompt" in ctx.data || "workflow" in ctx.data || "command" in ctx.data;
|