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,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;
|
|
@@ -2,6 +2,7 @@
|
|
|
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
4
|
import fs from "node:fs";
|
|
5
|
+
import { parseWorkflow } from "../../workflows/parser.js";
|
|
5
6
|
import { BaseLinter } from "./base-linter.js";
|
|
6
7
|
const PLACEHOLDER_STRINGS = ["Describe what this workflow accomplishes", "Example Workflow"];
|
|
7
8
|
/**
|
|
@@ -32,7 +33,7 @@ export class WorkflowLinter extends BaseLinter {
|
|
|
32
33
|
file: ctx.relPath,
|
|
33
34
|
issue: "placeholder-stub",
|
|
34
35
|
detail: `could not delete: ${e instanceof Error ? e.message : String(e)}`,
|
|
35
|
-
fixed:
|
|
36
|
+
fixed: "failed",
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
return issues;
|
|
@@ -44,6 +45,30 @@ export class WorkflowLinter extends BaseLinter {
|
|
|
44
45
|
fixed: false,
|
|
45
46
|
});
|
|
46
47
|
}
|
|
48
|
+
const isReadOnly = ctx.filePath.includes("/.cache/") || ctx.filePath.includes("/registry/");
|
|
49
|
+
if (!isReadOnly) {
|
|
50
|
+
try {
|
|
51
|
+
const result = parseWorkflow(ctx.raw, { path: ctx.filePath });
|
|
52
|
+
if (!result.ok) {
|
|
53
|
+
for (const err of result.errors ?? []) {
|
|
54
|
+
issues.push({
|
|
55
|
+
file: ctx.relPath,
|
|
56
|
+
issue: "invalid-workflow-structure",
|
|
57
|
+
detail: err.message ?? String(err),
|
|
58
|
+
fixed: false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
issues.push({
|
|
65
|
+
file: ctx.relPath,
|
|
66
|
+
issue: "invalid-workflow-structure",
|
|
67
|
+
detail: `workflow parser error: ${e instanceof Error ? e.message : String(e)}`,
|
|
68
|
+
fixed: false,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
47
72
|
return issues;
|
|
48
73
|
}
|
|
49
74
|
#checkPlaceholderStub(body) {
|
|
@@ -28,7 +28,7 @@ import { parsePositiveIntFlag } from "../cli/parse-args.js";
|
|
|
28
28
|
import { defineJsonCommand, output, parseAllFlagValues, runWithJsonErrors } from "../cli/shared.js";
|
|
29
29
|
import { closeDatabase, collectTagSetFromEntries, openExistingDatabase } from "../indexer/db/db.js";
|
|
30
30
|
import { EMBEDDED_HINTS, EMBEDDED_HINTS_FULL } from "../output/cli-hints.js";
|
|
31
|
-
import {
|
|
31
|
+
import { getOutputMode, parseDetailLevel } from "../output/context.js";
|
|
32
32
|
import { formatEventLine } from "../output/text.js";
|
|
33
33
|
import { getDirname } from "../runtime.js";
|
|
34
34
|
import { akmEventsList, akmEventsTail } from "./events.js";
|
|
@@ -90,9 +90,9 @@ const eventsTailCommand = defineCommand({
|
|
|
90
90
|
},
|
|
91
91
|
async run({ args }) {
|
|
92
92
|
await runWithJsonErrors(async () => {
|
|
93
|
-
const intervalMs = parsePositiveIntFlag(
|
|
94
|
-
const maxDurationMs = parsePositiveIntFlag(
|
|
95
|
-
const maxEvents = parsePositiveIntFlag(
|
|
93
|
+
const intervalMs = parsePositiveIntFlag(args["interval-ms"], "--interval-ms");
|
|
94
|
+
const maxDurationMs = parsePositiveIntFlag(args["max-duration-ms"], "--max-duration-ms");
|
|
95
|
+
const maxEvents = parsePositiveIntFlag(args["max-events"], "--max-events");
|
|
96
96
|
const mode = getOutputMode();
|
|
97
97
|
// In streaming text mode we want each event to print as soon as it
|
|
98
98
|
// arrives. The polling loop emits via `onEvent`; the final result is
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
import fs from "node:fs";
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { UsageError } from "../../core/errors.js";
|
|
20
|
-
import { PROPOSAL_SOURCES } from "./
|
|
20
|
+
import { PROPOSAL_SOURCES } from "./repository.js";
|
|
21
21
|
// Valid `generator` values for a drain rule are exactly the canonical proposal
|
|
22
|
-
// `source` values (see {@link PROPOSAL_SOURCES} in src/commands/proposal/
|
|
22
|
+
// `source` values (see {@link PROPOSAL_SOURCES} in src/commands/proposal/repository.ts). The
|
|
23
23
|
// engine matches rules via `policy.accept.find(r => r.generator === proposal.source)`,
|
|
24
24
|
// so a generator that is not a real source can never match — it would be a
|
|
25
25
|
// silent permanent no-op. Validate against the closed set to surface typos.
|
|
@@ -39,12 +39,20 @@ const GeneratorSchema = z.enum(PROPOSAL_SOURCES, {
|
|
|
39
39
|
export const PERSONAL_STASH = {
|
|
40
40
|
name: "personal-stash",
|
|
41
41
|
accept: [
|
|
42
|
-
// Extract proposals carry freshly-pulled real content — accept when present
|
|
43
|
-
|
|
42
|
+
// Extract proposals carry freshly-pulled real content — accept when present,
|
|
43
|
+
// but cap the diff for parity with reflect(80)/consolidate(200): an
|
|
44
|
+
// arbitrarily large extract should not auto-promote with zero LLM calls.
|
|
45
|
+
{ generator: "extract", minContentLines: 1, maxDiffLines: 200 },
|
|
44
46
|
// Reflect refinements: accept small ones; larger refinements defer to review.
|
|
45
47
|
{ generator: "reflect", maxDiffLines: 80 },
|
|
46
48
|
// Consolidate within the diff band; mid-band lands in `defer` below.
|
|
47
49
|
{ generator: "consolidate", maxDiffLines: 200 },
|
|
50
|
+
// Recombine: accept only confirmed type:lesson proposals (promoted by the
|
|
51
|
+
// recombine confidence gate) within the diff band. type:hypothesis proposals
|
|
52
|
+
// have no matching rule here and stay pending on the audited-autonomous
|
|
53
|
+
// queue until recombine supersedes them or the 90-day TTL sweeps them —
|
|
54
|
+
// there is no manual-review rung (06-M3).
|
|
55
|
+
{ generator: "recombine", requireType: "lesson", maxDiffLines: 200 },
|
|
48
56
|
],
|
|
49
57
|
rejectEmpty: true,
|
|
50
58
|
// Mid-band consolidate, distill duplicates, and contradiction escalations are
|
|
@@ -83,6 +91,7 @@ const DrainAcceptRuleSchema = z
|
|
|
83
91
|
generator: GeneratorSchema,
|
|
84
92
|
maxDiffLines: z.number().int().positive().optional(),
|
|
85
93
|
minContentLines: z.number().int().nonnegative().optional(),
|
|
94
|
+
requireType: z.string().optional(),
|
|
86
95
|
})
|
|
87
96
|
.strict();
|
|
88
97
|
const DrainPolicySchema = z
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* the engine behind `akm proposal drain` and (later) the `triage` improve
|
|
10
10
|
* pre-pass.
|
|
11
11
|
*
|
|
12
|
-
* Design (see docs/
|
|
12
|
+
* Design (see docs/archive/proposal-triage-implementation-plan.md):
|
|
13
13
|
* - Reuses `listProposals` (no source filter — generator filtering is
|
|
14
14
|
* in-memory) and the `akmProposalAccept` / `akmProposalReject` wrappers from
|
|
15
15
|
* `proposal.ts` so the standard `promoted` / `rejected` events are emitted.
|
|
@@ -36,17 +36,15 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import fs from "node:fs";
|
|
38
38
|
import path from "node:path";
|
|
39
|
-
import { assertNever } from "../../core/assert.js";
|
|
40
39
|
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
41
40
|
import { resolveAssetPathFromName, TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
42
41
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
43
42
|
import { appendEvent } from "../../core/events.js";
|
|
44
43
|
import { info, warn } from "../../core/warn.js";
|
|
45
|
-
import {
|
|
46
|
-
import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
|
|
44
|
+
import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
|
|
47
45
|
import { chatCompletion, stripJsonFences } from "../../llm/client.js";
|
|
48
46
|
import { akmProposalAccept, akmProposalReject } from "./proposal.js";
|
|
49
|
-
import { listProposals, recordGateDecision } from "./
|
|
47
|
+
import { listProposals, recordGateDecision } from "./repository.js";
|
|
50
48
|
// ---------------------------------------------------------------------------
|
|
51
49
|
// Content helpers
|
|
52
50
|
// ---------------------------------------------------------------------------
|
|
@@ -80,7 +78,16 @@ export function classifyProposal(proposal, policy, maxDiffLines) {
|
|
|
80
78
|
if (policy.rejectEmpty && isEmptyDiff(proposal)) {
|
|
81
79
|
return { verdict: "reject", reason: "empty diff", gate: { reason: "empty-diff" } };
|
|
82
80
|
}
|
|
83
|
-
const rule = policy.accept.find((r) =>
|
|
81
|
+
const rule = policy.accept.find((r) => {
|
|
82
|
+
if (r.generator !== proposal.source)
|
|
83
|
+
return false;
|
|
84
|
+
if (r.requireType !== undefined) {
|
|
85
|
+
const fm = parseFrontmatter(proposal.payload.content ?? "").data;
|
|
86
|
+
if (typeof fm.type !== "string" || fm.type !== r.requireType)
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
84
91
|
if (rule) {
|
|
85
92
|
const lines = contentLineCount(content);
|
|
86
93
|
const body = contentBodyLineCount(content);
|
|
@@ -200,59 +207,38 @@ export function parseJudgmentVerdict(raw) {
|
|
|
200
207
|
return { decision, reason: typeof reason === "string" ? reason : "" };
|
|
201
208
|
}
|
|
202
209
|
/**
|
|
203
|
-
* Dispatch a single judgment prompt to the resolved runner
|
|
204
|
-
*
|
|
205
|
-
*
|
|
210
|
+
* Dispatch a single judgment prompt to the resolved runner via the unified
|
|
211
|
+
* {@link executeRunner} seam (X3). The `llm` arm is drain-specific (wraps
|
|
212
|
+
* `chatCompletion` — no filesystem) so it is supplied as the `llm` handler; the
|
|
213
|
+
* byte-identical `agent` / `sdk` arms route to the default profile runners (or
|
|
214
|
+
* the injected {@link JudgmentSeams} test fakes). A failed spawn warns and
|
|
215
|
+
* yields `null`, matching the prior per-arm behavior.
|
|
206
216
|
*/
|
|
207
217
|
async function dispatchJudgment(runner, prompt, seams) {
|
|
208
|
-
|
|
209
|
-
switch (runner.kind) {
|
|
210
|
-
case "llm": {
|
|
211
|
-
const messages = [{ role: "user", content: prompt }];
|
|
212
|
-
raw = seams.chat
|
|
213
|
-
? await seams.chat(runner, messages)
|
|
214
|
-
: await chatCompletion(runner.connection, messages, {
|
|
215
|
-
...(runner.timeoutMs !== undefined ? { timeoutMs: runner.timeoutMs } : {}),
|
|
216
|
-
});
|
|
217
|
-
break;
|
|
218
|
-
}
|
|
219
|
-
case "agent": {
|
|
220
|
-
const stdout = await runProfileJudgment(seams.runAgentFn ?? runAgent, runner.profile, prompt, "agent", runner.timeoutMs);
|
|
221
|
-
if (stdout === null)
|
|
222
|
-
return null;
|
|
223
|
-
raw = stdout;
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
case "sdk": {
|
|
227
|
-
const stdout = await runProfileJudgment(seams.runSdkFn ?? runOpencodeSdk, runner.profile, prompt, "sdk", runner.timeoutMs);
|
|
228
|
-
if (stdout === null)
|
|
229
|
-
return null;
|
|
230
|
-
raw = stdout;
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
default:
|
|
234
|
-
// Exhaustiveness arm (H1): a 4th RunnerSpec kind becomes a compile error
|
|
235
|
-
// here instead of leaving `raw` undefined at runtime.
|
|
236
|
-
return assertNever(runner);
|
|
237
|
-
}
|
|
238
|
-
return parseJudgmentVerdict(raw);
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Shared spawn-runner dispatch for the byte-identical `agent` / `sdk` arms: run
|
|
242
|
-
* the profile-based runner, log a labelled warning on failure, and return the
|
|
243
|
-
* captured stdout (or `null` on failure). `label` only changes the warn prefix.
|
|
244
|
-
*/
|
|
245
|
-
async function runProfileJudgment(run, profile, prompt, label, timeoutMs) {
|
|
246
|
-
const result = await run(profile, prompt, {
|
|
218
|
+
const runOptions = {
|
|
247
219
|
stdio: "captured",
|
|
248
220
|
parseOutput: "text",
|
|
249
|
-
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
221
|
+
...(runner.timeoutMs !== undefined ? { timeoutMs: runner.timeoutMs } : {}),
|
|
222
|
+
};
|
|
223
|
+
const result = await executeRunner(runner, prompt, runOptions, {
|
|
224
|
+
llm: async (spec, p) => {
|
|
225
|
+
const messages = [{ role: "user", content: p }];
|
|
226
|
+
const raw = seams.chat
|
|
227
|
+
? await seams.chat(spec, messages)
|
|
228
|
+
: await chatCompletion(spec.connection, messages, {
|
|
229
|
+
...(spec.timeoutMs !== undefined ? { timeoutMs: spec.timeoutMs } : {}),
|
|
230
|
+
});
|
|
231
|
+
// chatCompletion has no failure envelope — a returned string is success.
|
|
232
|
+
return { ok: true, exitCode: 0, stdout: raw, stderr: "", durationMs: 0 };
|
|
233
|
+
},
|
|
234
|
+
...(seams.runAgentFn ? { runAgent: seams.runAgentFn } : {}),
|
|
235
|
+
...(seams.runSdkFn ? { runSdk: seams.runSdkFn } : {}),
|
|
250
236
|
});
|
|
251
237
|
if (!result.ok) {
|
|
252
|
-
warn(`[triage] judgment ${
|
|
238
|
+
warn(`[triage] judgment ${runner.kind} failed: ${result.error ?? result.reason ?? "unknown error"}`);
|
|
253
239
|
return null;
|
|
254
240
|
}
|
|
255
|
-
return result.stdout;
|
|
241
|
+
return parseJudgmentVerdict(result.stdout);
|
|
256
242
|
}
|
|
257
243
|
/**
|
|
258
244
|
* Run the judgment tier over the deferred items. The runner only *judges*; the
|
|
@@ -361,6 +347,14 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
361
347
|
// — these are re-stamped `no-judge-configured` when no runner resolves them.
|
|
362
348
|
const needsJudge = new Set();
|
|
363
349
|
for (const proposal of pending) {
|
|
350
|
+
// Do NOT reclassify a proposal that was already conclusively stamped
|
|
351
|
+
// `auto-rejected` by a prior gate run (e.g. the improve confidence gate).
|
|
352
|
+
// Overwriting an authoritative rejection with `auto-accepted` would corrupt
|
|
353
|
+
// the audit trail and silently promote content the gate explicitly rejected.
|
|
354
|
+
// Such proposals remain pending until the TTL expires them — the queue is
|
|
355
|
+
// audited-autonomous; no manual-review rung exists (06-M3).
|
|
356
|
+
if (proposal.gateDecision?.outcome === "auto-rejected")
|
|
357
|
+
continue;
|
|
364
358
|
const decision = classifyProposal(proposal, opts.policy, opts.maxDiffLines);
|
|
365
359
|
if (decision === null)
|
|
366
360
|
continue;
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
* Legacy filesystem proposal import (#578).
|
|
6
|
+
*
|
|
7
|
+
* Before 0.9.0 proposals lived as per-uuid JSON directories under
|
|
8
|
+
* `<stashDir>/.akm/proposals/` (live) and `…/proposals/archive/` (archived).
|
|
9
|
+
* The first proposal operation against a stash imports any legacy
|
|
10
|
+
* `proposal.json` files into the `proposals` table (INSERT OR IGNORE keyed on
|
|
11
|
+
* the UUID, so re-runs never duplicate) and records the stash in
|
|
12
|
+
* `proposal_fs_imports` so later invocations skip the directory walk. The
|
|
13
|
+
* legacy files are left in place untouched — they are inert after import and
|
|
14
|
+
* may be removed by the operator at leisure.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { warn } from "../../core/warn.js";
|
|
19
|
+
import { hasImportedFsProposals, insertProposalIfAbsent, recordFsProposalsImport, } from "../../storage/repositories/proposals-repository.js";
|
|
20
|
+
/** Legacy (pre-0.9.0) proposal directory: `<stashDir>/.akm/proposals[/archive]`. */
|
|
21
|
+
function legacyProposalsRoot(stashDir, archive) {
|
|
22
|
+
const root = path.join(stashDir, ".akm", "proposals");
|
|
23
|
+
return archive ? path.join(root, "archive") : root;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One-shot import of legacy `proposal.json` files into the `proposals` table.
|
|
27
|
+
*
|
|
28
|
+
* Idempotent at two levels: the `proposal_fs_imports` ledger skips the
|
|
29
|
+
* directory walk after the first successful import, and INSERT OR IGNORE
|
|
30
|
+
* (keyed on the proposal UUID) protects against duplicates even if the walk
|
|
31
|
+
* re-runs. Legacy `backup.<ext>` files are inlined into `backupContent` so
|
|
32
|
+
* `akm proposal revert` keeps working for proposals accepted before 0.9.0.
|
|
33
|
+
*
|
|
34
|
+
* The legacy files are never modified or deleted — after import they are
|
|
35
|
+
* inert artifacts the operator can remove at leisure.
|
|
36
|
+
*/
|
|
37
|
+
export function importLegacyProposalFiles(db, stashDir) {
|
|
38
|
+
if (hasImportedFsProposals(db, stashDir))
|
|
39
|
+
return;
|
|
40
|
+
const liveRoot = legacyProposalsRoot(stashDir, false);
|
|
41
|
+
if (!fs.existsSync(liveRoot))
|
|
42
|
+
return;
|
|
43
|
+
let imported = 0;
|
|
44
|
+
for (const archive of [false, true]) {
|
|
45
|
+
const root = legacyProposalsRoot(stashDir, archive);
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (!entry.isDirectory() || entry.name === "archive")
|
|
55
|
+
continue;
|
|
56
|
+
const proposalDir = path.join(root, entry.name);
|
|
57
|
+
const proposal = readLegacyProposalFile(proposalDir);
|
|
58
|
+
if (!proposal)
|
|
59
|
+
continue;
|
|
60
|
+
if (insertProposalIfAbsent(db, proposal, stashDir))
|
|
61
|
+
imported += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
recordFsProposalsImport(db, stashDir, imported);
|
|
65
|
+
if (imported > 0) {
|
|
66
|
+
warn(`[proposals] imported ${imported} legacy proposal file(s) from ${liveRoot} into state.db`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Parse one legacy proposal directory into a {@link Proposal}, inlining the
|
|
71
|
+
* backup file (when present) as `backupContent`. Returns undefined — with a
|
|
72
|
+
* warning — when the `proposal.json` is missing, unreadable, or malformed, so
|
|
73
|
+
* a single corrupt legacy entry never blocks the import of the rest.
|
|
74
|
+
*/
|
|
75
|
+
function readLegacyProposalFile(proposalDir) {
|
|
76
|
+
const filePath = path.join(proposalDir, "proposal.json");
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
warn(`[proposals] skipping legacy proposal at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
if (typeof parsed !== "object" ||
|
|
86
|
+
parsed === null ||
|
|
87
|
+
typeof parsed.id !== "string" ||
|
|
88
|
+
typeof parsed.ref !== "string") {
|
|
89
|
+
warn(`[proposals] skipping legacy proposal at ${filePath}: not a proposal object`);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const { backup, ...rest } = parsed;
|
|
93
|
+
let backupContent;
|
|
94
|
+
if (typeof backup === "string" && backup.length > 0) {
|
|
95
|
+
try {
|
|
96
|
+
backupContent = fs.readFileSync(path.join(proposalDir, backup), "utf8");
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Backup file lost — import the proposal anyway; revert for it will
|
|
100
|
+
// surface "no backup available", same as a new-asset proposal.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
...rest,
|
|
105
|
+
payload: {
|
|
106
|
+
content: rest.payload?.content ?? "",
|
|
107
|
+
...(rest.payload?.frontmatter ? { frontmatter: rest.payload.frontmatter } : {}),
|
|
108
|
+
},
|
|
109
|
+
createdAt: rest.createdAt ?? "",
|
|
110
|
+
updatedAt: rest.updatedAt ?? rest.createdAt ?? "",
|
|
111
|
+
status: rest.status ?? "pending",
|
|
112
|
+
source: rest.source ?? "import",
|
|
113
|
+
...(backupContent !== undefined ? { backupContent } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|