akm-cli 0.9.0-beta.6 → 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 +663 -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 -14
- 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 +153 -147
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- 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 -1083
- 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 +1313 -1278
- 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 +243 -599
- 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 +55 -4
- 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 +160 -7
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +32 -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 +510 -2537
- 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 +115 -0
- 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 +212 -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 +7 -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 -882
- package/dist/commands/read/curate.js +419 -103
- 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 +29 -41
- 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 +389 -58
- 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 +21 -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 +142 -1091
- 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 +356 -780
- 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 +103 -24
- 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 +106 -0
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +291 -310
- 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 +11 -1
- 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 +57 -15
- 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 +2718 -2354
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
- 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/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/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
|
@@ -29,6 +29,7 @@ export const TYPE_TO_RENDERER = {
|
|
|
29
29
|
wiki: "wiki-md",
|
|
30
30
|
task: "task-yaml",
|
|
31
31
|
session: "session-md",
|
|
32
|
+
fact: "fact-md",
|
|
32
33
|
};
|
|
33
34
|
/** Map asset types to action builder functions for search results. */
|
|
34
35
|
export const ACTION_BUILDERS = {
|
|
@@ -40,11 +41,12 @@ export const ACTION_BUILDERS = {
|
|
|
40
41
|
lesson: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
|
|
41
42
|
memory: (ref) => `akm show ${ref} -> recall context`,
|
|
42
43
|
workflow: (ref) => buildWorkflowAction(ref),
|
|
43
|
-
env: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (
|
|
44
|
+
env: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (prefer --clean to minimize inherited parent env; child stdout is not redacted). akm env export ${ref} --out <file> writes a sourceable script (values to a file, not stdout).`,
|
|
44
45
|
secret: (ref) => `akm show ${ref} -> name only (value never shown); akm secret path ${ref} -> file path; akm secret run ${ref} <VAR> -- <command> -> run with value injected into $VAR`,
|
|
45
46
|
wiki: (ref) => `akm show ${ref} -> read the wiki page`,
|
|
46
47
|
task: (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`,
|
|
47
48
|
session: (ref) => `akm show ${ref} -> read the session summary; follow the \`access\` frontmatter to open the raw log at \`log_path\``,
|
|
49
|
+
fact: (ref) => `akm show ${ref} -> read the stash fact and apply it as durable context`,
|
|
48
50
|
};
|
|
49
51
|
/**
|
|
50
52
|
* Register a type-to-renderer mapping.
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { buildWorkflowAction } from "../../output/renderers.js";
|
|
6
|
-
import { toPosix } from "../common.js";
|
|
7
6
|
import { registerActionBuilder, registerTypeRenderer } from "./asset-registry.js";
|
|
7
|
+
function toPosix(input) {
|
|
8
|
+
return input.replace(/\\/g, "/");
|
|
9
|
+
}
|
|
8
10
|
const buildTaskAction = (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`;
|
|
9
11
|
const markdownSpec = {
|
|
10
12
|
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
|
|
@@ -67,8 +69,9 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
67
69
|
script: { stashDir: "scripts", ...scriptSpec },
|
|
68
70
|
memory: { stashDir: "memories", ...markdownSpec },
|
|
69
71
|
// Environment assets — whole `.env` files sourced/injected wholesale. Replaced
|
|
70
|
-
// the deprecated `vault` type (removed in 0.9.0).
|
|
71
|
-
//
|
|
72
|
+
// the deprecated `vault` type (removed in 0.9.0). Only key NAMES are surfaced
|
|
73
|
+
// as metadata; values and comment text are never read for indexing (comments
|
|
74
|
+
// routinely contain commented-out credentials).
|
|
72
75
|
env: {
|
|
73
76
|
stashDir: "env",
|
|
74
77
|
isRelevantFile: (fileName) => fileName === ".env" || fileName.endsWith(".env"),
|
|
@@ -89,7 +92,7 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
89
92
|
return path.join(typeRoot, name.endsWith(".env") ? name : `${name}.env`);
|
|
90
93
|
},
|
|
91
94
|
rendererName: "env-file",
|
|
92
|
-
actionBuilder: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (
|
|
95
|
+
actionBuilder: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (prefer --clean to minimize inherited parent env; child stdout is not redacted); akm env export ${ref} --out <file> -> write a sourceable script to a file`,
|
|
93
96
|
},
|
|
94
97
|
// Secrets — a single sensitive value used on its own for authentication (a
|
|
95
98
|
// PEM key, API token, TLS cert). Unlike `env` (a group of related .env
|
|
@@ -154,6 +157,20 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
154
157
|
rendererName: "session-md",
|
|
155
158
|
actionBuilder: (ref) => `akm show ${ref} -> read the session summary; follow the \`access\` frontmatter to open the raw log at \`log_path\``,
|
|
156
159
|
},
|
|
160
|
+
// Durable stash-level semantic knowledge — facts about the user, team, or
|
|
161
|
+
// project (personal details, team tool stacks, coding conventions /
|
|
162
|
+
// "constitution", and stash-meta like naming conventions or the active
|
|
163
|
+
// projects list). Unlike `memory` (episodic, recency-decayed) these are
|
|
164
|
+
// mostly-static declarations meant to be reliably surfaced as context. A
|
|
165
|
+
// plain markdown spec; `category` frontmatter scopes the fact and
|
|
166
|
+
// `pinned: true` marks the small always-injected core. See
|
|
167
|
+
// docs/design/fact-asset-type.md.
|
|
168
|
+
fact: {
|
|
169
|
+
stashDir: "facts",
|
|
170
|
+
...markdownSpec,
|
|
171
|
+
rendererName: "fact-md",
|
|
172
|
+
actionBuilder: (ref) => `akm show ${ref} -> read the stash fact and apply it as durable context`,
|
|
173
|
+
},
|
|
157
174
|
};
|
|
158
175
|
export const ASSET_SPECS = ASSET_SPECS_INTERNAL;
|
|
159
176
|
/**
|
|
@@ -4,170 +4,45 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Shared frontmatter parsing utilities.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Uses the `yaml` library for all YAML parsing so that the full YAML spec
|
|
8
|
+
* (block scalars, multi-line strings, nested objects, flow sequences, escape
|
|
9
|
+
* sequences) is handled correctly without a brittle hand-rolled state machine.
|
|
9
10
|
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
13
|
+
import { assembleAsset } from "./asset-serialize.js";
|
|
10
14
|
/**
|
|
11
|
-
* Parse YAML
|
|
15
|
+
* Parse YAML frontmatter from a Markdown (or similar) string.
|
|
12
16
|
*
|
|
13
17
|
* Returns the parsed key-value data and the remaining body content.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - **Top-level values**: string, boolean, and number scalars are supported,
|
|
18
|
-
* as well as top-level list-valued keys using YAML block sequences
|
|
19
|
-
* (`- item`) or flow arrays (`[a, b, c]`).
|
|
20
|
-
* - **List item types**: list items must be scalar values and may be strings,
|
|
21
|
-
* booleans, or numbers.
|
|
22
|
-
* - **No nested objects beyond one level**: Only a single level of indented
|
|
23
|
-
* key-value pairs is supported.
|
|
24
|
-
* - **Block scalars**: `|` (literal), `|-` (strip), and `|+` (keep) block
|
|
25
|
-
* scalars are supported for multi-line string values as emitted by the
|
|
26
|
-
* `yaml` library's `stringify`.
|
|
18
|
+
* Delegates all YAML parsing to the `yaml` library; the only responsibility
|
|
19
|
+
* of this function is extracting the `---…---` block and normalizing the
|
|
20
|
+
* parsed result (e.g. converting YAML timestamp values to ISO date strings).
|
|
27
21
|
*/
|
|
28
22
|
export function parseFrontmatter(raw) {
|
|
29
23
|
const parsedBlock = parseFrontmatterBlock(raw);
|
|
30
24
|
if (!parsedBlock) {
|
|
31
25
|
return { data: {}, content: raw, frontmatter: null, bodyStartLine: 1 };
|
|
32
26
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/** Lines collected while in "block" mode. */
|
|
44
|
-
let blockLines = null;
|
|
45
|
-
/** Block scalar chomping: "clip" (|), "strip" (|-), "keep" (|+). */
|
|
46
|
-
let blockChomping = "clip";
|
|
47
|
-
const flushPending = () => {
|
|
48
|
-
// Called when we start a new top-level key and the previous key was still "pending".
|
|
49
|
-
// An empty-value key followed by another top-level key means it was an empty scalar.
|
|
50
|
-
if (mode === "pending" && currentKey !== null) {
|
|
51
|
-
data[currentKey] = "";
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
const flushBlock = () => {
|
|
55
|
-
// Commit the accumulated block-scalar lines to `data[currentKey]`.
|
|
56
|
-
if (mode !== "block" || currentKey === null || blockLines === null)
|
|
57
|
-
return;
|
|
58
|
-
// De-indent: strip the common 2-space prefix `yaml.stringify` emits.
|
|
59
|
-
const deindented = blockLines.map((l) => (l.startsWith(" ") ? l.slice(2) : l));
|
|
60
|
-
// Chomping: apply trailing-newline policy.
|
|
61
|
-
// "clip" (|): single trailing newline.
|
|
62
|
-
// "strip" (|-): no trailing newline.
|
|
63
|
-
// "keep" (|+): keep all trailing newlines as-is.
|
|
64
|
-
if (blockChomping === "keep") {
|
|
65
|
-
data[currentKey] = deindented.join("\n");
|
|
66
|
-
}
|
|
67
|
-
else if (blockChomping === "strip") {
|
|
68
|
-
data[currentKey] = deindented.join("\n").replace(/\n+$/, "");
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
// "clip": exactly one trailing newline
|
|
72
|
-
data[currentKey] = `${deindented.join("\n").replace(/\n+$/, "")}\n`;
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
for (const line of parsedBlock.frontmatter.split(/\r?\n/)) {
|
|
76
|
-
// If we are in block-scalar mode, collect indented lines or end the block.
|
|
77
|
-
if (mode === "block") {
|
|
78
|
-
if (line.startsWith(" ") || line === "") {
|
|
79
|
-
// Continuation of the block scalar (indented content or blank line).
|
|
80
|
-
blockLines.push(line);
|
|
81
|
-
continue;
|
|
27
|
+
let data = {};
|
|
28
|
+
if (parsedBlock.frontmatter.trim()) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = yamlParse(parsedBlock.frontmatter);
|
|
31
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
32
|
+
// Normalize Date objects: the yaml "core" schema parses YYYY-MM-DD
|
|
33
|
+
// literals as JS Date instances. Convert them back to ISO date strings
|
|
34
|
+
// to preserve the string type that callers (and yaml.stringify on write)
|
|
35
|
+
// expect.
|
|
36
|
+
data = normalizeYamlValues(parsed);
|
|
82
37
|
}
|
|
83
|
-
// Non-indented line ends the block scalar — flush and fall through to
|
|
84
|
-
// parse the new line as a top-level key.
|
|
85
|
-
flushBlock();
|
|
86
|
-
mode = "scalar";
|
|
87
|
-
blockLines = null;
|
|
88
|
-
}
|
|
89
|
-
// Block-sequence item: "- value" or " - value" (optional 2-space indent)
|
|
90
|
-
// Only match when the current key is in list or pending mode.
|
|
91
|
-
const seqItem = line.match(/^(?: {2})?- (.*)$/);
|
|
92
|
-
if (seqItem && currentKey !== null && (mode === "list" || mode === "pending")) {
|
|
93
|
-
if (mode === "pending") {
|
|
94
|
-
// First block-sequence item after an empty-value key — switch to list mode
|
|
95
|
-
currentList = [];
|
|
96
|
-
data[currentKey] = currentList;
|
|
97
|
-
mode = "list";
|
|
98
|
-
}
|
|
99
|
-
currentList.push(parseYamlScalar(seqItem[1].trim()));
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
// Plain-style multi-line scalar continuation: a 2-space-indented line that
|
|
103
|
-
// is not a sequence item or nested key. YAML plain scalars fold newlines
|
|
104
|
-
// into a single space, so we append with a space. This handles LLM-emitted
|
|
105
|
-
// descriptions like:
|
|
106
|
-
// description: Use 4-colon outer containers when mixing
|
|
107
|
-
// nesting depths in markdown-it-container plugins.
|
|
108
|
-
// Without this, only the first line is captured and the truncation
|
|
109
|
-
// heuristic wrongly flags it as cut off mid-sentence.
|
|
110
|
-
if (mode === "scalar" && currentKey !== null && /^ {2}\S/.test(line)) {
|
|
111
|
-
data[currentKey] = `${String(data[currentKey])} ${line.trim()}`;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
// Indented nested key-value (object under a key with empty value)
|
|
115
|
-
const indented = line.match(/^ {2}(\w[\w-]*):\s*(.+)$/);
|
|
116
|
-
if (indented && currentKey !== null && (mode === "object" || mode === "pending")) {
|
|
117
|
-
if (mode === "pending") {
|
|
118
|
-
// First indented k-v after an empty-value key — switch to object mode
|
|
119
|
-
nested = {};
|
|
120
|
-
data[currentKey] = nested;
|
|
121
|
-
mode = "object";
|
|
122
|
-
}
|
|
123
|
-
nested[indented[1]] = parseYamlScalar(indented[2].trim());
|
|
124
|
-
continue;
|
|
125
38
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Starting a new top-level key — flush any pending empty-value key
|
|
132
|
-
flushPending();
|
|
133
|
-
currentKey = top[1];
|
|
134
|
-
const value = top[2].trim();
|
|
135
|
-
if (value === "|" || value === "|-" || value === "|+") {
|
|
136
|
-
// Block scalar header — collect subsequent indented lines.
|
|
137
|
-
mode = "block";
|
|
138
|
-
blockLines = [];
|
|
139
|
-
blockChomping = value === "|-" ? "strip" : value === "|+" ? "keep" : "clip";
|
|
140
|
-
nested = null;
|
|
141
|
-
currentList = null;
|
|
142
|
-
}
|
|
143
|
-
else if (value === "") {
|
|
144
|
-
// Defer mode decision until we see the next line
|
|
145
|
-
mode = "pending";
|
|
146
|
-
nested = null;
|
|
147
|
-
currentList = null;
|
|
148
|
-
// Don't store anything yet — flushPending will set "" if no continuation
|
|
149
|
-
}
|
|
150
|
-
else if (value.startsWith("[") && value.endsWith("]")) {
|
|
151
|
-
// Inline flow array: tags: [ops, networking]
|
|
152
|
-
mode = "list";
|
|
153
|
-
nested = null;
|
|
154
|
-
currentList = null;
|
|
155
|
-
currentList = parseFlowArray(value);
|
|
156
|
-
data[currentKey] = currentList;
|
|
39
|
+
catch {
|
|
40
|
+
// Malformed YAML (e.g. unterminated quotes from LLM output corruption).
|
|
41
|
+
// Fall back to line-by-line best-effort extraction so callers still get
|
|
42
|
+
// whatever scalar values they can rather than a completely empty record.
|
|
43
|
+
data = parseFrontmatterLenient(parsedBlock.frontmatter);
|
|
157
44
|
}
|
|
158
|
-
else {
|
|
159
|
-
mode = "scalar";
|
|
160
|
-
nested = null;
|
|
161
|
-
currentList = null;
|
|
162
|
-
data[currentKey] = parseYamlScalar(value);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// Flush any in-progress block scalar at end of frontmatter.
|
|
166
|
-
if (mode === "block") {
|
|
167
|
-
flushBlock();
|
|
168
45
|
}
|
|
169
|
-
// Flush the last key if it was still pending (empty value, no continuation)
|
|
170
|
-
flushPending();
|
|
171
46
|
return {
|
|
172
47
|
data,
|
|
173
48
|
content: parsedBlock.content,
|
|
@@ -176,29 +51,109 @@ export function parseFrontmatter(raw) {
|
|
|
176
51
|
};
|
|
177
52
|
}
|
|
178
53
|
/**
|
|
179
|
-
*
|
|
54
|
+
* Normalize YAML-parsed values to match expected AKM frontmatter types.
|
|
55
|
+
*
|
|
56
|
+
* Two conversions:
|
|
57
|
+
* 1. `Date` → YYYY-MM-DD string: the yaml "core" schema parses bare date
|
|
58
|
+
* scalars like `2026-06-18` as JS Date instances. AKM frontmatter treats
|
|
59
|
+
* `updated:` and similar fields as plain strings.
|
|
60
|
+
* 2. `null` → `""`: the yaml library parses empty-value keys (`key:` with no
|
|
61
|
+
* value) as `null`, but AKM callers historically received `""` from the
|
|
62
|
+
* hand-rolled parser. Convert to preserve backward compatibility.
|
|
180
63
|
*/
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
64
|
+
function normalizeYamlValues(value) {
|
|
65
|
+
if (value instanceof Date) {
|
|
66
|
+
const y = value.getUTCFullYear();
|
|
67
|
+
const m = String(value.getUTCMonth() + 1).padStart(2, "0");
|
|
68
|
+
const d = String(value.getUTCDate()).padStart(2, "0");
|
|
69
|
+
return `${y}-${m}-${d}`;
|
|
70
|
+
}
|
|
71
|
+
if (value === null)
|
|
72
|
+
return "";
|
|
73
|
+
if (Array.isArray(value))
|
|
74
|
+
return value.map(normalizeYamlValues);
|
|
75
|
+
if (typeof value === "object") {
|
|
76
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, normalizeYamlValues(v)]));
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Best-effort line-by-line frontmatter extraction for malformed YAML.
|
|
82
|
+
*
|
|
83
|
+
* Used as a fallback when yaml.parse throws (e.g. unterminated quotes from LLM
|
|
84
|
+
* output corruption). Extracts simple `key: value` scalar pairs only — nested
|
|
85
|
+
* objects and sequences are skipped. Values that are individually parseable by
|
|
86
|
+
* yaml are normalized; otherwise stored as raw strings.
|
|
87
|
+
*/
|
|
88
|
+
function parseFrontmatterLenient(frontmatter) {
|
|
89
|
+
const data = {};
|
|
90
|
+
for (const line of frontmatter.split(/\r?\n/)) {
|
|
91
|
+
const m = line.match(/^([\w][\w-]*):\s*(.*)$/);
|
|
92
|
+
if (!m)
|
|
93
|
+
continue;
|
|
94
|
+
const key = m[1];
|
|
95
|
+
const rawValue = (m[2] ?? "").trim();
|
|
96
|
+
try {
|
|
97
|
+
const singleEntry = yamlParse(`k: ${rawValue}`);
|
|
98
|
+
if (singleEntry !== null && typeof singleEntry === "object" && !Array.isArray(singleEntry)) {
|
|
99
|
+
const v = singleEntry.k;
|
|
100
|
+
data[key] = v === null || v === undefined ? "" : v;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
data[key] = rawValue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
data[key] = rawValue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Read a file, parse its frontmatter, let `mutator` compute the next
|
|
114
|
+
* frontmatter object, and write the reassembled asset back to disk.
|
|
115
|
+
*
|
|
116
|
+
* This is the shared read→parse→mutate→write primitive. The `mutator` receives
|
|
117
|
+
* the parsed result and returns either the next frontmatter object (to write)
|
|
118
|
+
* or `null` to skip the write entirely (e.g. for idempotent no-ops). The body
|
|
119
|
+
* content is preserved from the parse.
|
|
120
|
+
*
|
|
121
|
+
* @returns `true` if a write occurred, `false` if the mutator returned `null`.
|
|
122
|
+
*/
|
|
123
|
+
export function mutateFrontmatter(filePath, mutator) {
|
|
124
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
125
|
+
const parsed = parseFrontmatter(raw);
|
|
126
|
+
const nextFrontmatter = mutator(parsed);
|
|
127
|
+
if (nextFrontmatter === null)
|
|
128
|
+
return false;
|
|
129
|
+
fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
|
|
130
|
+
return true;
|
|
186
131
|
}
|
|
187
132
|
export function parseFrontmatterBlock(raw) {
|
|
188
133
|
// Handle both LF and CRLF line endings throughout.
|
|
189
134
|
// The closing --- may be preceded by \r\n; capture and strip trailing \r
|
|
190
135
|
// from the frontmatter block so key parsing sees clean LF-terminated lines.
|
|
191
136
|
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r\n|\r|\n|$)([\s\S]*)$/);
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
137
|
+
if (match) {
|
|
138
|
+
// Strip any \r characters from the frontmatter block to normalise CRLF → LF
|
|
139
|
+
const frontmatter = match[1].replace(/\r/g, "");
|
|
140
|
+
const content = match[2];
|
|
141
|
+
return {
|
|
142
|
+
frontmatter,
|
|
143
|
+
content,
|
|
144
|
+
bodyStartLine: countLines(raw.slice(0, match[0].length - match[2].length)) + 1,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Empty frontmatter (---\n---): the content-bearing regex above requires at
|
|
148
|
+
// least one character between the fences. Handle the degenerate case so
|
|
149
|
+
// callers can reconstruct `---\nkey: val\n---\n\nbody` from a previously
|
|
150
|
+
// empty-frontmatter file without corrupting it by wrapping the entire raw
|
|
151
|
+
// string as body content.
|
|
152
|
+
const emptyMatch = raw.match(/^---\r?\n---(?:\r\n|\r|\n)([\s\S]*)$/);
|
|
153
|
+
if (emptyMatch) {
|
|
154
|
+
return { frontmatter: "", content: emptyMatch[1], bodyStartLine: 3 };
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
202
157
|
}
|
|
203
158
|
function countLines(text) {
|
|
204
159
|
if (text.length === 0)
|
|
@@ -206,7 +161,13 @@ function countLines(text) {
|
|
|
206
161
|
return text.split(/\r?\n/).length - 1;
|
|
207
162
|
}
|
|
208
163
|
/**
|
|
209
|
-
* Parse a
|
|
164
|
+
* Parse a YAML scalar value (string, boolean, or number).
|
|
165
|
+
*
|
|
166
|
+
* For quoted strings (single or double), delegates to the `yaml` library so
|
|
167
|
+
* escape sequences are handled correctly per spec. The previous hand-rolled
|
|
168
|
+
* `slice(1, -1)` only stripped one layer of quoting and left inner quotes and
|
|
169
|
+
* escape sequences as literal characters in the stored value, causing visible
|
|
170
|
+
* corruption when `yaml.stringify` re-quoted them on the next write.
|
|
210
171
|
*/
|
|
211
172
|
export function parseYamlScalar(value) {
|
|
212
173
|
if (value === "")
|
|
@@ -219,7 +180,67 @@ export function parseYamlScalar(value) {
|
|
|
219
180
|
if (!Number.isNaN(asNumber))
|
|
220
181
|
return asNumber;
|
|
221
182
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
183
|
+
try {
|
|
184
|
+
const parsed = yamlParse(value);
|
|
185
|
+
if (typeof parsed === "string")
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Fall through to raw slice on malformed YAML — better than throwing.
|
|
190
|
+
}
|
|
222
191
|
return value.slice(1, -1);
|
|
223
192
|
}
|
|
224
193
|
return value;
|
|
225
194
|
}
|
|
195
|
+
// ── Minimum score delta to trigger a frontmatter salience rewrite ─────────────
|
|
196
|
+
const SALIENCE_WRITE_DELTA_THRESHOLD = 0.05;
|
|
197
|
+
/**
|
|
198
|
+
* Idempotently write `salience` and `salienceInputs` fields into the YAML
|
|
199
|
+
* frontmatter of a raw asset string.
|
|
200
|
+
*
|
|
201
|
+
* Skips the write when the existing `salience` field differs from `score` by
|
|
202
|
+
* less than {@link SALIENCE_WRITE_DELTA_THRESHOLD}, to avoid churn for minor
|
|
203
|
+
* floating-point drift. Returns the raw string unchanged when no write is needed
|
|
204
|
+
* or when no frontmatter block is present.
|
|
205
|
+
*
|
|
206
|
+
* The `salienceInputs` field is written for auditability only; no pipeline code
|
|
207
|
+
* reads it back. `state.db :: asset_salience` is the canonical store.
|
|
208
|
+
*/
|
|
209
|
+
export function writeSalienceToFrontmatter(raw, score, inputs) {
|
|
210
|
+
const parsed = parseFrontmatterBlock(raw);
|
|
211
|
+
if (!parsed)
|
|
212
|
+
return raw;
|
|
213
|
+
const existingData = parseFrontmatter(raw).data;
|
|
214
|
+
const existingSalience = typeof existingData.salience === "number" ? existingData.salience : undefined;
|
|
215
|
+
if (existingSalience !== undefined && Math.abs(existingSalience - score) < SALIENCE_WRITE_DELTA_THRESHOLD) {
|
|
216
|
+
return raw;
|
|
217
|
+
}
|
|
218
|
+
// Parse existing frontmatter into an object, then set/overwrite salience fields.
|
|
219
|
+
let fm = {};
|
|
220
|
+
if (parsed.frontmatter.trim()) {
|
|
221
|
+
try {
|
|
222
|
+
const p = yamlParse(parsed.frontmatter);
|
|
223
|
+
if (p !== null && typeof p === "object" && !Array.isArray(p)) {
|
|
224
|
+
fm = p;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Malformed YAML — rebuild from best-effort parse
|
|
229
|
+
fm = parseFrontmatterLenient(parsed.frontmatter);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
fm.salience = roundTo2dp(score);
|
|
233
|
+
fm.salienceInputs = {
|
|
234
|
+
novelty: roundTo2dp(inputs.novelty),
|
|
235
|
+
magnitude: roundTo2dp(inputs.magnitude),
|
|
236
|
+
predictionError: roundTo2dp(inputs.predictionError),
|
|
237
|
+
};
|
|
238
|
+
const newFrontmatter = yamlStringify(fm).trimEnd();
|
|
239
|
+
const body = parsed.content;
|
|
240
|
+
// Preserve original line ending style between frontmatter and body
|
|
241
|
+
const separator = body.startsWith("\n") ? "" : "\n";
|
|
242
|
+
return `---\n${newFrontmatter}\n---\n${separator}${body}`;
|
|
243
|
+
}
|
|
244
|
+
function roundTo2dp(n) {
|
|
245
|
+
return Math.round(n * 100) / 100;
|
|
246
|
+
}
|
|
@@ -8,7 +8,15 @@ export function parseMarkdownToc(content) {
|
|
|
8
8
|
const headings = [];
|
|
9
9
|
const parsed = parseFrontmatter(content);
|
|
10
10
|
const start = parsed.frontmatter ? parsed.bodyStartLine - 1 : 0;
|
|
11
|
+
let inFence = false;
|
|
11
12
|
for (let i = start; i < lines.length; i++) {
|
|
13
|
+
// Track fenced code blocks (``` or ~~~) so headings inside them are skipped.
|
|
14
|
+
if (/^\s*(`{3,}|~{3,})/.test(lines[i])) {
|
|
15
|
+
inFence = !inFence;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (inFence)
|
|
19
|
+
continue;
|
|
12
20
|
const match = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
|
13
21
|
if (match) {
|
|
14
22
|
headings.push({
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
* Canonical HARD authoring rules — the single source of truth shared by the
|
|
6
|
+
* proposal validators (which REJECT violations) and the improve/authoring
|
|
7
|
+
* prompts (which must TELL the agent the same rules, in the same words).
|
|
8
|
+
*
|
|
9
|
+
* Why this module exists: authoring rules were duplicated and drifted across
|
|
10
|
+
* prompt templates. `distill-lesson-system.md` told the model "80–200 chars"
|
|
11
|
+
* while the validator enforced 20–400; reflect's prompt omitted the
|
|
12
|
+
* no-pseudo-frontmatter / single-fence rules entirely, so reflect generated
|
|
13
|
+
* proposals that the gate then rejected and that got stuck in the queue.
|
|
14
|
+
*
|
|
15
|
+
* The fix: the numeric bounds live HERE and are imported by both the validators
|
|
16
|
+
* (`isValidDescription` / `isValidWhenToUse` in proposal-quality-validators.ts)
|
|
17
|
+
* and the prompt text (`authoringRulesForType`). The agent-facing rule prose
|
|
18
|
+
* sits next to the bounds it describes, so a developer changing a validator
|
|
19
|
+
* sees the prompt copy that must change with it. `tests/authoring-rules-*`
|
|
20
|
+
* asserts the two representations stay consistent.
|
|
21
|
+
*
|
|
22
|
+
* SCOPE: only HARD rules (a validator rejects the proposal if violated) belong
|
|
23
|
+
* here. Soft/style conventions (voice, paragraph count, "include a # Title")
|
|
24
|
+
* are user-editable and flow through the separate `standardsContext` seam
|
|
25
|
+
* (stash `category: convention` facts) — NOT this module.
|
|
26
|
+
*/
|
|
27
|
+
// ── Canonical numeric bounds (imported by the validators — do not duplicate) ──
|
|
28
|
+
/** `description` length bounds (chars). Enforced by `isValidDescription`. */
|
|
29
|
+
export const DESCRIPTION_MIN_CHARS = 20;
|
|
30
|
+
export const DESCRIPTION_MAX_CHARS = 400;
|
|
31
|
+
/** `when_to_use` length bounds (chars). Enforced by `isValidWhenToUse`. */
|
|
32
|
+
export const WHEN_TO_USE_MIN_CHARS = 15;
|
|
33
|
+
export const WHEN_TO_USE_MAX_CHARS = 400;
|
|
34
|
+
// ── Agent-facing rule prose (mirrors the validator checks one-for-one) ────────
|
|
35
|
+
/**
|
|
36
|
+
* Rules that apply to any markdown asset authored with YAML frontmatter + a
|
|
37
|
+
* body. Enforced by `detectDoubleFrontmatter` (currently fires for lesson
|
|
38
|
+
* proposals, but the rules are universally correct, so we state them for every
|
|
39
|
+
* type to prevent the same defect class elsewhere).
|
|
40
|
+
*/
|
|
41
|
+
const FRONTMATTER_BODY_RULES = [
|
|
42
|
+
"Emit EXACTLY TWO `---` fence lines — the opening and closing of the YAML frontmatter. Do NOT use `---` as a horizontal rule anywhere in the body.",
|
|
43
|
+
"Do NOT restate `description:` or `when_to_use:` inside the body (no `**description:** …` or `**when_to_use:** …` lines). Those keys belong in the frontmatter ONLY.",
|
|
44
|
+
];
|
|
45
|
+
/** Rules for the `description` frontmatter field. Enforced by `isValidDescription`. */
|
|
46
|
+
const DESCRIPTION_RULES = [
|
|
47
|
+
`\`description\` must be ${DESCRIPTION_MIN_CHARS}–${DESCRIPTION_MAX_CHARS} characters of plain-prose sentence — no leading digit or markdown marker, balanced backticks, and it must NOT end with \`:\`, \`;\`, or \`,\` (those read as truncation).`,
|
|
48
|
+
'`description` must NOT be a section-heading fragment (e.g. "Overview", "Key points", "Summary"), a code fragment (must not start with `def`/`function`/`class`/`const`/…), or end on a hanging connector word ("a", "the", "and", "to", …).',
|
|
49
|
+
"`description` must NOT merely restate the asset's ref/name; write what the asset actually does.",
|
|
50
|
+
'`description` should NOT start with "When" — that phrasing belongs in `when_to_use`.',
|
|
51
|
+
];
|
|
52
|
+
/** Rules for the `when_to_use` frontmatter field. Enforced by `isValidWhenToUse`. */
|
|
53
|
+
const WHEN_TO_USE_RULES = [
|
|
54
|
+
`\`when_to_use\` is REQUIRED and must be ${WHEN_TO_USE_MIN_CHARS}–${WHEN_TO_USE_MAX_CHARS} characters describing a concrete trigger. Never write the circular fallback "When working with <name>".`,
|
|
55
|
+
"`description` and `when_to_use` must be different from each other.",
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Asset types that carry a `description` and a body where the
|
|
59
|
+
* frontmatter/body rules apply. (Types without those — if any are added later —
|
|
60
|
+
* simply fall through to the cross-cutting block.)
|
|
61
|
+
*/
|
|
62
|
+
const DESCRIPTION_TYPES = new Set(["lesson", "knowledge", "memory", "skill", "command", "agent", "workflow", "fact"]);
|
|
63
|
+
/** Types where `when_to_use` is a HARD requirement (validator rejects if absent). */
|
|
64
|
+
const WHEN_TO_USE_TYPES = new Set(["lesson"]);
|
|
65
|
+
/**
|
|
66
|
+
* Build the hard-rules block for a given asset type, ready to inject as a prompt
|
|
67
|
+
* section. Returns `""` for an unknown type (no over-claiming). The block is
|
|
68
|
+
* deterministic so prompt snapshots stay stable.
|
|
69
|
+
*
|
|
70
|
+
* Inject this VERBATIM into every improve/authoring prompt that creates or edits
|
|
71
|
+
* an asset of `type`, so the agent is told exactly what the gate will reject.
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* Whether an asset of `type` carries a `description` that the validator
|
|
75
|
+
* (`isValidDescription` / `validateProposalFrontmatter`) treats as required.
|
|
76
|
+
* Reflect uses this to decide when a description-synthesis instruction (and the
|
|
77
|
+
* deterministic reflect-side fallback) must fire for a description-less source.
|
|
78
|
+
*/
|
|
79
|
+
export function requiresDescription(type) {
|
|
80
|
+
return DESCRIPTION_TYPES.has(type);
|
|
81
|
+
}
|
|
82
|
+
export function authoringRulesForType(type) {
|
|
83
|
+
const rules = [...FRONTMATTER_BODY_RULES];
|
|
84
|
+
if (DESCRIPTION_TYPES.has(type))
|
|
85
|
+
rules.push(...DESCRIPTION_RULES);
|
|
86
|
+
if (WHEN_TO_USE_TYPES.has(type))
|
|
87
|
+
rules.push(...WHEN_TO_USE_RULES);
|
|
88
|
+
if (rules.length === 0)
|
|
89
|
+
return "";
|
|
90
|
+
const heading = `Hard authoring rules for ${type} assets (the validator REJECTS proposals that violate these):`;
|
|
91
|
+
return [heading, ...rules.map((r) => `- ${r}`)].join("\n");
|
|
92
|
+
}
|
package/dist/core/common.js
CHANGED
|
@@ -44,11 +44,6 @@ export function isRemoteUrl(value) {
|
|
|
44
44
|
value.startsWith("ssh://") ||
|
|
45
45
|
value.startsWith("git://"));
|
|
46
46
|
}
|
|
47
|
-
export function filterNonEmptyStrings(value) {
|
|
48
|
-
if (!Array.isArray(value))
|
|
49
|
-
return undefined;
|
|
50
|
-
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
51
|
-
}
|
|
52
47
|
// ── Validators ──────────────────────────────────────────────────────────────
|
|
53
48
|
/**
|
|
54
49
|
* Returns true if `type` is a known asset type — either a built-in from
|
|
@@ -89,14 +84,7 @@ export function writeFileAtomic(target, content, mode) {
|
|
|
89
84
|
const tmp = `${target}.tmp.${process.pid}.${crypto.randomBytes(8).toString("hex")}`;
|
|
90
85
|
const fd = fs.openSync(tmp, "w", mode ?? 0o600);
|
|
91
86
|
try {
|
|
92
|
-
|
|
93
|
-
// so each call resolves to a single overload. Both write byte-exact.
|
|
94
|
-
if (typeof content === "string") {
|
|
95
|
-
fs.writeSync(fd, content);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
fs.writeSync(fd, content);
|
|
99
|
-
}
|
|
87
|
+
fs.writeSync(fd, typeof content === "string" ? Buffer.from(content) : content);
|
|
100
88
|
try {
|
|
101
89
|
fs.fdatasyncSync(fd);
|
|
102
90
|
}
|
|
@@ -137,9 +125,9 @@ export function writeFileAtomic(target, content, mode) {
|
|
|
137
125
|
*
|
|
138
126
|
* Throws if no valid stash directory is found.
|
|
139
127
|
*/
|
|
140
|
-
export function resolveStashDir(_options) {
|
|
128
|
+
export function resolveStashDir(_options, env = process.env) {
|
|
141
129
|
// 1. Env var override (for CI, scripts, testing)
|
|
142
|
-
const envDir =
|
|
130
|
+
const envDir = env.AKM_STASH_DIR?.trim();
|
|
143
131
|
if (envDir) {
|
|
144
132
|
return validateStashDir(envDir);
|
|
145
133
|
}
|
|
@@ -148,7 +136,7 @@ export function resolveStashDir(_options) {
|
|
|
148
136
|
if (configStashDir)
|
|
149
137
|
return validateStashDir(configStashDir);
|
|
150
138
|
// 3. Platform default — use it if it exists
|
|
151
|
-
const defaultDir = getDefaultStashDir();
|
|
139
|
+
const defaultDir = getDefaultStashDir(env);
|
|
152
140
|
if (isValidDirectory(defaultDir)) {
|
|
153
141
|
return defaultDir;
|
|
154
142
|
}
|
|
@@ -467,13 +455,6 @@ export function asNonEmptyString(value) {
|
|
|
467
455
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
468
456
|
}
|
|
469
457
|
// ── Generic data utilities ───────────────────────────────────────────────────
|
|
470
|
-
/**
|
|
471
|
-
* Return the trimmed string if non-empty, otherwise `undefined`.
|
|
472
|
-
* Equivalent to `firstString` previously defined in `memory-improve.ts`.
|
|
473
|
-
*/
|
|
474
|
-
export function firstString(value) {
|
|
475
|
-
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
476
|
-
}
|
|
477
458
|
/**
|
|
478
459
|
* Coerce an unknown value to a filtered, trimmed string array.
|
|
479
460
|
* Non-strings and empty/whitespace-only entries are dropped.
|