akm-cli 0.9.0-rc.0 → 0.9.0-rc.13
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 +1283 -22
- package/README.md +62 -37
- package/SECURITY.md +46 -31
- package/dist/akm +162 -38
- package/dist/akm-migrate +44 -0
- package/dist/assets/backends/schtasks-template.xml +2 -1
- package/dist/assets/hints/cli-hints-full.md +268 -118
- package/dist/assets/hints/cli-hints-short.md +87 -24
- package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
- package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
- package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
- package/dist/assets/improve-strategies/frequent.json +15 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
- package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
- package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
- package/dist/assets/improve-strategies/reflect-distill.json +30 -0
- package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +5 -5
- package/dist/assets/prompts/extract-session.md +2 -6
- package/dist/assets/prompts/memory-infer-user.md +2 -3
- package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
- package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
- package/dist/assets/prompts/reflect-output-repair.md +3 -0
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +38 -10
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
- package/dist/assets/tasks/core/extract.yml +3 -2
- package/dist/assets/tasks/core/improve.yml +2 -1
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +2 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
- package/dist/assets/templates/html/health.html +5 -4
- package/dist/assets/workflows/workflow-template.md +31 -15
- package/dist/cli/invocation.js +279 -0
- package/dist/cli/parse-args.js +5 -90
- package/dist/cli/retired-commands.js +78 -0
- package/dist/cli/shared.js +158 -48
- package/dist/cli-node.mjs +2 -1
- package/dist/cli.js +747 -293
- package/dist/commands/agent/agent-dispatch.js +19 -18
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +43 -97
- package/dist/commands/completions.js +80 -23
- package/dist/commands/config-cli.js +44 -281
- package/dist/commands/env/env-binding.js +99 -0
- package/dist/commands/env/env-cli.js +84 -224
- package/dist/commands/env/env.js +12 -163
- package/dist/commands/env/marker-path.js +6 -0
- package/dist/commands/env/secret-cli.js +45 -61
- package/dist/commands/env/secret.js +32 -62
- package/dist/commands/feedback-cli.js +179 -85
- package/dist/commands/health/accept-rate.js +58 -0
- package/dist/commands/health/advisories.js +7 -8
- package/dist/commands/health/checks.js +279 -94
- package/dist/commands/health/html-report.js +197 -578
- package/dist/commands/health/improve-metrics.js +277 -246
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +16 -7
- package/dist/commands/health/metrics.js +67 -32
- package/dist/commands/health/renderers.js +47 -0
- package/dist/commands/health/report-view-model.js +508 -0
- package/dist/commands/health/stash-exposure.js +1 -1
- package/dist/commands/health/surfaces.js +16 -56
- package/dist/commands/health/task-runs.js +3 -67
- package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
- package/dist/commands/health/types-improve.js +29 -0
- package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
- package/dist/commands/health/types-result.js +7 -0
- package/dist/commands/health/types-runs.js +4 -0
- package/dist/commands/health/types-session-log.js +4 -0
- package/dist/commands/health/types-windows.js +4 -0
- package/dist/commands/health/types.js +26 -21
- package/dist/commands/health/windows.js +2 -3
- package/dist/commands/health.js +296 -167
- package/dist/commands/improve/anti-collapse.js +5 -5
- package/dist/commands/improve/autonomy-gate.js +68 -0
- package/dist/commands/improve/collapse-detector.js +65 -52
- package/dist/commands/improve/consolidate/chunking.js +9 -7
- package/dist/commands/improve/consolidate/eligibility.js +1 -23
- package/dist/commands/improve/consolidate/merge.js +4 -0
- package/dist/commands/improve/consolidate.js +454 -1354
- package/dist/commands/improve/content-hash.js +39 -0
- package/dist/commands/improve/distill/content-repair.js +4 -10
- package/dist/commands/improve/distill/promote-memory.js +89 -64
- package/dist/commands/improve/distill/quality-gate.js +118 -42
- package/dist/commands/improve/distill-guards.js +1 -1
- package/dist/commands/improve/distill-promotion-policy.js +33 -888
- package/dist/commands/improve/distill.js +607 -363
- package/dist/commands/improve/eligibility.js +165 -79
- package/dist/commands/improve/extract-cli.js +35 -126
- package/dist/commands/improve/extract-prompt.js +6 -35
- package/dist/commands/improve/extract.js +640 -391
- package/dist/commands/improve/feedback-valence.js +2 -12
- package/dist/commands/improve/improve-cli.js +134 -135
- package/dist/commands/improve/improve-result-file.js +30 -50
- package/dist/commands/improve/improve-run-types.js +4 -0
- package/dist/commands/improve/improve-strategies.js +135 -0
- package/dist/commands/improve/improve.js +904 -701
- package/dist/commands/improve/locks.js +64 -111
- package/dist/commands/improve/loop-stages.js +1110 -923
- package/dist/commands/improve/memory/derived-ref.js +124 -0
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
- package/dist/commands/improve/memory/memory-improve.js +25 -37
- package/dist/commands/improve/outcome-loop.js +25 -88
- package/dist/commands/improve/preparation.js +1034 -813
- package/dist/commands/improve/proactive-maintenance.js +34 -9
- package/dist/commands/improve/proposal-envelope.js +31 -0
- package/dist/commands/improve/reflect.js +983 -794
- package/dist/commands/improve/run-context.js +119 -0
- package/dist/commands/improve/salience.js +24 -127
- package/dist/commands/improve/session-asset.js +7 -3
- package/dist/commands/improve/shared.js +14 -34
- package/dist/commands/improve/source-identity.js +28 -0
- package/dist/commands/improve/triage.js +20 -17
- package/dist/commands/lint/base-linter.js +340 -313
- package/dist/commands/lint/env-key-rules.js +31 -47
- package/dist/commands/lint/index.js +185 -30
- package/dist/commands/{events.js → log.js} +28 -38
- package/dist/commands/migrate-cli.js +54 -0
- package/dist/commands/migration-tool.js +55 -0
- package/dist/commands/observability-cli.js +70 -208
- package/dist/commands/proposal/diff-format.js +50 -0
- package/dist/commands/proposal/drain-policies.js +0 -6
- package/dist/commands/proposal/drain.js +91 -40
- package/dist/commands/proposal/proposal-cli.js +134 -132
- package/dist/commands/proposal/proposal-types.js +56 -0
- package/dist/commands/proposal/proposal.js +83 -65
- package/dist/commands/proposal/propose-cli.js +88 -0
- package/dist/commands/proposal/propose.js +105 -88
- package/dist/commands/proposal/repository.js +1303 -278
- package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
- package/dist/commands/proposal/validators/proposal-validators.js +61 -12
- package/dist/commands/proposal/validators/proposals.js +6 -8
- package/dist/commands/read/curate.js +78 -73
- package/dist/commands/read/knowledge.js +510 -13
- package/dist/commands/read/registry-search.js +2 -2
- package/dist/commands/read/remember-cli.js +84 -15
- package/dist/commands/read/search-cli.js +203 -96
- package/dist/commands/read/search.js +126 -94
- package/dist/commands/read/show.js +226 -250
- package/dist/commands/registry-cli.js +34 -60
- package/dist/commands/remember.js +18 -57
- package/dist/commands/sources/add-cli.js +104 -49
- package/dist/commands/sources/bundle-cli.js +166 -0
- package/dist/commands/sources/bundle-config-ops.js +63 -0
- package/dist/commands/sources/info.js +27 -15
- package/dist/commands/sources/init.js +30 -40
- package/dist/commands/sources/installed-stashes.js +469 -172
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +10 -9
- package/dist/commands/sources/self-update.js +182 -121
- package/dist/commands/sources/source-add.js +169 -178
- package/dist/commands/sources/source-clone.js +144 -41
- package/dist/commands/sources/source-manage.js +94 -59
- package/dist/commands/sources/sources-cli.js +64 -205
- package/dist/commands/sources/stash-cli.js +91 -54
- package/dist/commands/sources/stash-skeleton.js +1 -1
- package/dist/commands/tasks/tasks-cli.js +106 -104
- package/dist/commands/tasks/tasks.js +445 -262
- package/dist/commands/workflow-cli.js +232 -121
- package/dist/core/action-contributors.js +1 -1
- package/dist/core/activation-policy.js +49 -0
- package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
- package/dist/core/adapter/adapters/akm-adapter.js +528 -0
- package/dist/core/adapter/adapters/akm-lint.js +392 -0
- package/dist/core/adapter/adapters/akm-metadata.js +387 -0
- package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
- package/dist/core/adapter/adapters/claude-adapter.js +61 -0
- package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
- package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
- package/dist/core/adapter/adapters/index.js +80 -0
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
- package/dist/core/adapter/adapters/okf-adapter.js +391 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
- package/dist/core/adapter/adapters/shared.js +286 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
- package/dist/core/adapter/bundle-adapter.js +4 -0
- package/dist/core/adapter/detect-adapter.js +17 -0
- package/dist/core/adapter/recognize-match.js +44 -0
- package/dist/core/adapter/registry.js +56 -0
- package/dist/core/adapter/types.js +4 -0
- package/dist/core/asset/akm-markdown.js +30 -0
- package/dist/core/asset/asset-placement.js +243 -0
- package/dist/core/asset/asset-ref.js +110 -79
- package/dist/core/asset/asset-serialize.js +20 -0
- package/dist/core/asset/frontmatter.js +28 -12
- package/dist/core/asset/markdown.js +40 -51
- package/dist/core/asset/resolve-ref.js +274 -0
- package/dist/core/asset/stash-meta.js +2 -2
- package/dist/core/bundle-id.js +51 -0
- package/dist/core/common.js +281 -86
- package/dist/core/config/config-io.js +42 -128
- package/dist/core/config/config-schema.js +233 -834
- package/dist/core/config/config-sources.js +162 -39
- package/dist/core/config/config-types.js +16 -11
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +126 -37
- package/dist/core/config/config.js +154 -331
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +28 -0
- package/dist/core/config/experimental.js +21 -0
- package/dist/core/config/schema/embedding.js +38 -0
- package/dist/core/config/schema/engines.js +116 -0
- package/dist/core/config/schema/experimental.js +47 -0
- package/dist/core/config/schema/feedback.js +31 -0
- package/dist/core/config/schema/improve-processes.js +389 -0
- package/dist/core/config/schema/improve.js +94 -0
- package/dist/core/config/schema/index-config.js +176 -0
- package/dist/core/config/schema/output.js +18 -0
- package/dist/core/config/schema/primitives.js +94 -0
- package/dist/core/config/schema/search.js +30 -0
- package/dist/core/config/schema/setup.js +18 -0
- package/dist/core/config/schema/sources-bundles.js +169 -0
- package/dist/core/config/schema/workflow.js +29 -0
- package/dist/core/env-secret-ref.js +155 -20
- package/dist/core/errors.js +17 -15
- package/dist/core/events-types.js +4 -0
- package/dist/core/events.js +46 -128
- package/dist/core/extra-params.js +62 -0
- package/dist/core/file-change.js +17 -0
- package/dist/core/file-lock.js +202 -57
- package/dist/core/fs-txn.js +392 -0
- package/dist/core/git-message.js +59 -0
- package/dist/core/improve-result.js +167 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/lesson-lint.js +1 -17
- package/dist/core/logs-db.js +1 -1
- package/dist/core/maintenance-barrier.js +135 -0
- package/dist/core/migration-operation.js +44 -0
- package/dist/core/mutation-target.js +78 -0
- package/dist/core/paths.js +22 -25
- package/dist/core/platform.js +10 -0
- package/dist/core/recognition-util.js +128 -0
- package/dist/core/redaction.js +392 -0
- package/dist/core/standards/resolve-standards-context.js +36 -65
- package/dist/core/standards/resolve-stash-standards.js +2 -2
- package/dist/core/standards/resolve-type-conventions.js +5 -5
- package/dist/core/state/migrations.js +242 -11
- package/dist/core/state-db.js +98 -10
- package/dist/core/structured.js +1 -1
- package/dist/core/subprocess.js +303 -0
- package/dist/core/text-truncation.js +9 -5
- package/dist/core/time.js +20 -0
- package/dist/core/type-presentation.js +130 -0
- package/dist/core/warn.js +0 -3
- package/dist/core/write-source.js +834 -118
- package/dist/indexer/bundle-identity-guard.js +92 -0
- package/dist/indexer/db/graph-db.js +1 -25
- package/dist/indexer/db/llm-cache.js +1 -1
- package/dist/indexer/ensure-index.js +30 -9
- package/dist/indexer/graph/graph-boost.js +9 -30
- package/dist/indexer/graph/graph-extraction.js +41 -27
- package/dist/indexer/graph/graph-types.js +4 -0
- package/dist/indexer/index-writer-lock.js +93 -49
- package/dist/indexer/index-written-assets.js +100 -53
- package/dist/indexer/indexer.js +746 -329
- package/dist/indexer/init.js +18 -25
- package/dist/indexer/installations.js +142 -0
- package/dist/indexer/passes/dir-staleness.js +18 -10
- package/dist/indexer/passes/memory-inference.js +25 -15
- package/dist/indexer/passes/metadata.js +412 -243
- package/dist/indexer/scan/doc-to-entry.js +160 -0
- package/dist/indexer/scan/drain-dir.js +134 -0
- package/dist/indexer/search/db-search.js +292 -108
- package/dist/indexer/search/fts-query.js +64 -0
- package/dist/indexer/search/ranking-contributors.js +145 -25
- package/dist/indexer/search/ranking-types.js +4 -0
- package/dist/indexer/search/ranking.js +28 -71
- package/dist/indexer/search/search-attribution.js +67 -0
- package/dist/indexer/search/search-fields.js +18 -3
- package/dist/indexer/search/search-hit-enrichers.js +30 -40
- package/dist/indexer/search/search-source.js +157 -111
- package/dist/indexer/search/semantic-status.js +4 -1
- package/dist/indexer/usage/usage-events.js +10 -30
- package/dist/indexer/walk/file-context.js +3 -45
- package/dist/indexer/walk/matchers.js +42 -34
- package/dist/indexer/walk/path-resolver.js +11 -5
- package/dist/indexer/walk/walker.js +42 -14
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -56
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +231 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/prompts.js +46 -18
- package/dist/integrations/agent/runner-dispatch.js +93 -4
- package/dist/integrations/agent/runner.js +76 -208
- package/dist/integrations/agent/spawn.js +88 -196
- package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
- package/dist/integrations/harnesses/aider/index.js +48 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
- package/dist/integrations/harnesses/amazonq/index.js +45 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/config-import.js +1 -3
- package/dist/integrations/harnesses/claude/index.js +24 -35
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/claude/session-log.js +27 -75
- package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
- package/dist/integrations/harnesses/codex/index.js +52 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
- package/dist/integrations/harnesses/copilot/index.js +48 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +48 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/ids.js +24 -0
- package/dist/integrations/harnesses/index.js +54 -34
- package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
- package/dist/integrations/harnesses/opencode/config-import.js +1 -3
- package/dist/integrations/harnesses/opencode/index.js +14 -32
- package/dist/integrations/harnesses/opencode/session-log.js +67 -125
- package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
- package/dist/integrations/harnesses/openhands/index.js +48 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
- package/dist/integrations/harnesses/pi/index.js +45 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/shared.js +17 -0
- package/dist/integrations/harnesses/types.js +43 -32
- package/dist/integrations/lockfile.js +211 -24
- package/dist/integrations/session-logs/index.js +36 -39
- package/dist/integrations/session-logs/provider-base.js +113 -0
- package/dist/llm/client.js +182 -110
- package/dist/llm/embedders/deterministic.js +2 -2
- package/dist/llm/embedders/remote.js +21 -9
- package/dist/llm/feature-gate.js +17 -57
- package/dist/llm/graph-extract.js +12 -13
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer.js +144 -1
- package/dist/llm/metadata-enhance.js +45 -30
- package/dist/llm/structured-call.js +16 -8
- package/dist/llm/usage-persist.js +30 -5
- package/dist/llm/usage-telemetry.js +59 -6
- package/dist/output/cli-hints.js +1 -2
- package/dist/output/command-registry.js +27 -0
- package/dist/output/context.js +22 -7
- package/dist/output/format-exempt.js +80 -0
- package/dist/output/generic-render.js +251 -0
- package/dist/output/html-render.js +11 -16
- package/dist/output/render-registry.js +57 -0
- package/dist/output/renderers.js +14 -279
- package/dist/output/shapes/curate.js +10 -1
- package/dist/output/shapes/events.js +12 -7
- package/dist/output/shapes/helpers.js +58 -84
- package/dist/output/shapes/passthrough.js +11 -39
- package/dist/output/shapes/proposal/producer.js +15 -7
- package/dist/output/shapes/registry.js +12 -6
- package/dist/output/shapes.js +0 -9
- package/dist/output/text/{init.js → bundle-create.js} +3 -1
- package/dist/output/text/bundle-show.js +7 -0
- package/dist/output/text/command-format.js +562 -0
- package/dist/output/text/env.js +1 -3
- package/dist/output/text/events.js +8 -7
- package/dist/output/text/helpers.js +15 -1164
- package/dist/output/text/proposal/producer.js +4 -2
- package/dist/output/text/proposal-format.js +202 -0
- package/dist/output/text/registry-commands.js +1 -2
- package/dist/output/text/registry.js +12 -6
- package/dist/output/text/show-directives.js +117 -0
- package/dist/output/text/show-format.js +103 -0
- package/dist/output/text/sync.js +5 -0
- package/dist/output/text/workflow-format.js +332 -0
- package/dist/output/text/workflow.js +3 -2
- package/dist/output/text.js +10 -19
- package/dist/registry/factory.js +4 -6
- package/dist/registry/origin-resolve.js +16 -27
- package/dist/registry/providers/skills-sh.js +3 -3
- package/dist/registry/providers/static-index.js +15 -25
- package/dist/registry/resolve.js +43 -94
- package/dist/registry/semver.js +43 -0
- package/dist/runtime.js +81 -12
- package/dist/scripts/akm-migrate.js +35529 -0
- package/dist/setup/detect.js +5 -7
- package/dist/setup/detected-engines.js +136 -0
- package/dist/setup/engine-config.js +100 -0
- package/dist/setup/registry-stash-loader.js +3 -3
- package/dist/setup/semantic-assets.js +12 -9
- package/dist/setup/setup.js +444 -208
- package/dist/setup/steps/connection-shared.js +120 -0
- package/dist/setup/steps/connection.js +108 -305
- package/dist/setup/steps/platforms.js +13 -12
- package/dist/setup/steps/semantic.js +15 -3
- package/dist/setup/steps/sources.js +21 -15
- package/dist/setup/steps/stashdir.js +6 -4
- package/dist/setup/steps/tasks.js +236 -119
- package/dist/setup/steps.js +3 -2
- package/dist/sources/freshness.js +39 -0
- package/dist/sources/provider-factory.js +11 -17
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +278 -34
- package/dist/sources/providers/git-provider.js +54 -56
- package/dist/sources/providers/git-stash.js +420 -91
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/providers/npm.js +16 -19
- package/dist/sources/providers/provider-utils.js +47 -22
- package/dist/sources/providers/sync-from-ref.js +3 -9
- package/dist/sources/providers/website.js +2 -2
- package/dist/sources/resolve.js +11 -10
- package/dist/sources/snapshot-fetchers/types.js +4 -0
- package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
- package/dist/storage/database.js +60 -4
- package/dist/storage/engines/sqlite-migrations.js +156 -5
- package/dist/storage/locations.js +1 -2
- package/dist/storage/repositories/canaries-repository.js +1 -1
- package/dist/storage/repositories/events-repository.js +51 -11
- package/dist/storage/repositories/improve-runs-repository.js +6 -32
- package/dist/storage/repositories/index-connection.js +79 -0
- package/dist/storage/repositories/index-db.js +4 -3
- package/dist/storage/repositories/index-entries-repository.js +863 -0
- package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
- package/dist/storage/repositories/index-entry-types.js +4 -0
- package/dist/storage/repositories/index-fts-repository.js +167 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
- package/dist/storage/repositories/index-meta-repository.js +49 -0
- package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
- package/dist/storage/repositories/index-sql.js +12 -0
- package/dist/storage/repositories/index-utility-repository.js +356 -0
- package/dist/storage/repositories/index-vec-repository.js +250 -0
- package/dist/storage/repositories/outcome-repository.js +119 -0
- package/dist/storage/repositories/proposals-repository.js +317 -75
- package/dist/storage/repositories/registry-cache.js +1 -1
- package/dist/storage/repositories/salience-repository.js +172 -0
- package/dist/storage/repositories/task-history-repository.js +110 -3
- package/dist/storage/repositories/workflow-runs-repository.js +240 -19
- package/dist/tasks/backends/cron.js +169 -46
- package/dist/tasks/backends/exec-utils.js +76 -3
- package/dist/tasks/backends/index.js +6 -9
- package/dist/tasks/backends/launchd.js +292 -55
- package/dist/tasks/backends/schtasks.js +557 -70
- package/dist/tasks/backends/types.js +4 -0
- package/dist/tasks/command-executable.js +93 -0
- package/dist/tasks/embedded.js +56 -38
- package/dist/tasks/parser.js +156 -64
- package/dist/tasks/resolve-akm-bin.js +144 -51
- package/dist/tasks/runner.js +377 -209
- package/dist/tasks/schedule.js +108 -19
- package/dist/tasks/scheduler-invocation.js +296 -0
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/task-id.js +35 -0
- package/dist/tasks/validator.js +30 -16
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/authoring/authoring.js +104 -43
- package/dist/workflows/authoring/scope-key.js +1 -1
- package/dist/workflows/cli.js +0 -16
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/exec/brief.js +450 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1038 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1460 -0
- package/dist/workflows/exec/run-workflow.js +602 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1190 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/workflow-engine-gate.js +67 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +246 -0
- package/dist/workflows/ir/freeze.js +233 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +540 -0
- package/dist/workflows/parser.js +878 -304
- package/dist/workflows/program/expressions.js +181 -0
- package/dist/workflows/program/schema.js +51 -0
- package/dist/workflows/renderer.js +100 -45
- package/dist/workflows/resource-limits.js +22 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/checkin.js +1 -1
- package/dist/workflows/runtime/plan-classifier.js +131 -0
- package/dist/workflows/runtime/runs.js +376 -119
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/unit-phases.js +20 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
- package/dist/workflows/schema.js +1 -11
- package/dist/workflows/validate-summary.js +2 -3
- package/dist/workflows/validator.js +52 -30
- package/docs/README.md +42 -78
- package/docs/migration/README.md +8 -0
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +9 -8
- package/docs/migration/release-notes/0.9.0.md +158 -14
- package/docs/migration/v0.7-to-v0.8.md +46 -47
- package/docs/migration/v0.8-to-v0.9.md +844 -0
- package/docs/reference/README.md +12 -0
- package/docs/reference/data-and-telemetry.md +333 -0
- package/package.json +21 -17
- package/schemas/akm-asset-envelope.json +93 -0
- package/schemas/akm-config.json +4636 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +373 -0
- package/dist/akm-migrate-storage +0 -38
- package/dist/assets/help/help-accept.md +0 -12
- package/dist/assets/help/help-improve.md +0 -84
- package/dist/assets/help/help-proposals.md +0 -17
- package/dist/assets/help/help-propose.md +0 -17
- package/dist/assets/help/help-reject.md +0 -11
- package/dist/assets/profiles/frequent.json +0 -13
- package/dist/assets/profiles/recombine-only.json +0 -21
- package/dist/assets/profiles/reflect-distill.json +0 -30
- package/dist/assets/profiles/synthesize.json +0 -15
- package/dist/assets/prompts/procedural-system.md +0 -44
- package/dist/assets/prompts/recombine-system.md +0 -40
- package/dist/assets/prompts/staleness-detect-system.md +0 -6
- package/dist/assets/tasks/core/backup.yml +0 -4
- package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
- package/dist/assets/templates/html/default.html +0 -78
- package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
- package/dist/assets/wiki/index-template.md +0 -12
- package/dist/assets/wiki/ingest-workflow-template.md +0 -83
- package/dist/assets/wiki/log-template.md +0 -8
- package/dist/assets/wiki/schema-template.md +0 -61
- package/dist/cli/config-migrate.js +0 -150
- package/dist/cli/config-validate.js +0 -39
- package/dist/commands/graph/graph-cli.js +0 -124
- package/dist/commands/graph/graph.js +0 -487
- package/dist/commands/improve/calibration.js +0 -161
- package/dist/commands/improve/dedup.js +0 -482
- package/dist/commands/improve/extract-watch.js +0 -140
- package/dist/commands/improve/hot-probation.js +0 -45
- package/dist/commands/improve/improve-auto-accept.js +0 -276
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/commands/improve/procedural.js +0 -398
- package/dist/commands/improve/recombine.js +0 -818
- package/dist/commands/improve/schema-similarity-gate.js +0 -168
- package/dist/commands/lint/agent-linter.js +0 -44
- package/dist/commands/lint/command-linter.js +0 -44
- package/dist/commands/lint/default-linter.js +0 -16
- package/dist/commands/lint/fact-linter.js +0 -39
- package/dist/commands/lint/knowledge-linter.js +0 -16
- package/dist/commands/lint/memory-linter.js +0 -61
- package/dist/commands/lint/registry.js +0 -41
- package/dist/commands/lint/skill-linter.js +0 -45
- package/dist/commands/lint/task-linter.js +0 -50
- package/dist/commands/lint/workflow-linter.js +0 -81
- package/dist/commands/proposal/legacy-import.js +0 -115
- package/dist/commands/sources/history.js +0 -196
- package/dist/commands/tasks/default-tasks.js +0 -186
- package/dist/commands/wiki-cli.js +0 -292
- package/dist/core/asset/asset-registry.js +0 -76
- package/dist/core/asset/asset-spec.js +0 -259
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/core/eval/rank-metrics.js +0 -113
- package/dist/core/ripgrep/install.js +0 -163
- package/dist/core/ripgrep/resolve.js +0 -81
- package/dist/indexer/db/db.js +0 -1413
- package/dist/indexer/manifest.js +0 -170
- package/dist/indexer/passes/metadata-contributors.js +0 -31
- package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
- package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
- package/dist/llm/call-ai.js +0 -62
- package/dist/llm/memory-infer-impl.js +0 -138
- package/dist/output/shapes/distill.js +0 -14
- package/dist/output/shapes/history.js +0 -11
- package/dist/output/text/distill.js +0 -6
- package/dist/output/text/enable-disable.js +0 -8
- package/dist/output/text/history.js +0 -6
- package/dist/output/text/wiki.js +0 -16
- package/dist/registry/build-index.js +0 -386
- package/dist/scripts/migrate-storage.js +0 -19108
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
- package/dist/scripts/migrations/v16-to-v17.js +0 -141
- package/dist/setup/legacy-config.js +0 -106
- package/dist/storage/repositories/consolidation-repository.js +0 -38
- package/dist/storage/repositories/recombine-repository.js +0 -213
- package/dist/wiki/wiki-templates.js +0 -15
- package/dist/wiki/wiki.js +0 -1012
- package/dist/workflows/db.js +0 -215
- package/docs/data-and-telemetry.md +0 -226
- /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
- /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* content. Pull recent feedback (`feedback` events for that ref) and
|
|
12
12
|
* lesson-lint findings to surface as schema hints.
|
|
13
13
|
* 3. Build the prompt via {@link buildReflectPrompt}.
|
|
14
|
-
* 4.
|
|
14
|
+
* 4. Dispatch the selected named engine via {@link executeRunner}.
|
|
15
15
|
* 5. Parse the agent's stdout into a {@link AgentProposalPayload}.
|
|
16
16
|
* 6. Insert into the proposal queue via {@link createProposal} with
|
|
17
17
|
* `source: "reflect"`.
|
|
@@ -24,32 +24,36 @@
|
|
|
24
24
|
import fs from "node:fs";
|
|
25
25
|
import os from "node:os";
|
|
26
26
|
import path from "node:path";
|
|
27
|
-
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
28
27
|
import { assembleAssetFromString, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
|
|
29
28
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
30
|
-
import {
|
|
29
|
+
import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
31
30
|
import { DESCRIPTION_MAX_CHARS, requiresDescription } from "../../core/authoring-rules.js";
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
31
|
+
import { loadConfig } from "../../core/config/config.js";
|
|
32
|
+
import { ConfigError } from "../../core/errors.js";
|
|
35
33
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
36
34
|
import { lintLessonContent } from "../../core/lesson-lint.js";
|
|
35
|
+
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
36
|
+
import { redactSensitiveText } from "../../core/redaction.js";
|
|
37
37
|
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
38
|
-
import { warn } from "../../core/warn.js";
|
|
39
38
|
import { lookup } from "../../indexer/indexer.js";
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import { buildReflectPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "../../integrations/agent/prompts.js";
|
|
43
|
-
import {
|
|
44
|
-
import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import { createProposal, isProposalSkipped, listProposals, } from "../proposal/repository.js";
|
|
39
|
+
import { DEFAULT_LLM_TIMEOUT_MS } from "../../integrations/agent/config.js";
|
|
40
|
+
import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
|
|
41
|
+
import { buildReflectOutputRepairPrompt, buildReflectPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "../../integrations/agent/prompts.js";
|
|
42
|
+
import { materializeLlmRunnerConnection, resolveImproveProcessRunner, runnerIsLlm, runnerSupportsFileWrite, } from "../../integrations/agent/runner.js";
|
|
43
|
+
import { collectDispatchSensitiveValues, executeRunner } from "../../integrations/agent/runner-dispatch.js";
|
|
44
|
+
import { LlmCallError } from "../../llm/client.js";
|
|
45
|
+
import { callStructured } from "../../llm/structured-call.js";
|
|
46
|
+
import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js";
|
|
47
|
+
import { isProposalSkipped, listProposals, proposalContent, } from "../proposal/repository.js";
|
|
50
48
|
import { checkReflectSize, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
|
|
51
|
-
import { deriveLessonRef
|
|
49
|
+
import { deriveLessonRef } from "./distill.js";
|
|
50
|
+
import { runReflectQualityJudge } from "./distill/quality-gate.js";
|
|
51
|
+
import { findAssetFilePath } from "./eligibility.js";
|
|
52
|
+
import { emitProposal } from "./proposal-envelope.js";
|
|
52
53
|
import { classifyReflectChange } from "./reflect-noise.js";
|
|
54
|
+
import { createRunContext, resolveRunStashDir } from "./run-context.js";
|
|
55
|
+
import { MAX_REJECTED_PROPOSALS } from "./shared.js";
|
|
56
|
+
import { durableImproveRef, improveStateReadRefs } from "./source-identity.js";
|
|
53
57
|
const MAX_FEEDBACK_LINES = 10;
|
|
54
58
|
const MAX_GLOBAL_FEEDBACK_LINES = 20;
|
|
55
59
|
/**
|
|
@@ -60,10 +64,10 @@ const MAX_GLOBAL_FEEDBACK_LINES = 20;
|
|
|
60
64
|
*/
|
|
61
65
|
function readRecentFeedback(ref) {
|
|
62
66
|
try {
|
|
63
|
-
const
|
|
67
|
+
const events = readEvents({ type: "feedback", ...(ref ? { ref } : {}) }).events;
|
|
64
68
|
const lines = [];
|
|
65
69
|
const limit = ref ? MAX_FEEDBACK_LINES : MAX_GLOBAL_FEEDBACK_LINES;
|
|
66
|
-
for (const event of
|
|
70
|
+
for (const event of events.slice(-limit)) {
|
|
67
71
|
const md = (event.metadata ?? {});
|
|
68
72
|
const signal = typeof md.signal === "string" ? md.signal : "?";
|
|
69
73
|
const note = typeof md.reason === "string" ? md.reason : typeof md.note === "string" ? md.note : "";
|
|
@@ -76,7 +80,6 @@ function readRecentFeedback(ref) {
|
|
|
76
80
|
return [];
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
|
-
const MAX_REJECTED_PROPOSALS = 3;
|
|
80
83
|
/**
|
|
81
84
|
* Asset types that reflect is allowed to operate on.
|
|
82
85
|
*
|
|
@@ -97,7 +100,6 @@ export const REFLECT_ALLOWED_TYPES = new Set([
|
|
|
97
100
|
"knowledge",
|
|
98
101
|
"memory",
|
|
99
102
|
"lesson",
|
|
100
|
-
"wiki",
|
|
101
103
|
"skill",
|
|
102
104
|
"agent",
|
|
103
105
|
"command",
|
|
@@ -112,7 +114,7 @@ export const REFLECT_ALLOWED_TYPES = new Set([
|
|
|
112
114
|
* LLM tried to rewrite them.
|
|
113
115
|
*
|
|
114
116
|
* Observed regression: proposal `26941510` (May 2026) renamed
|
|
115
|
-
* `
|
|
117
|
+
* `skills/openpalm-stack-diagnostics`'s `name` field to `"diagnostic-checklist"`.
|
|
116
118
|
*/
|
|
117
119
|
const PROTECTED_FRONTMATTER_FIELDS = new Set(["name", "ref", "id", "slug", "type"]);
|
|
118
120
|
/**
|
|
@@ -132,7 +134,7 @@ function readRejectedProposals(stash, ref) {
|
|
|
132
134
|
.map((p) => ({
|
|
133
135
|
ref: p.ref,
|
|
134
136
|
reason: p.review?.reason ?? "no reason given",
|
|
135
|
-
contentPreview: p.
|
|
137
|
+
contentPreview: proposalContent(p).slice(0, 500),
|
|
136
138
|
}));
|
|
137
139
|
}
|
|
138
140
|
catch {
|
|
@@ -142,19 +144,20 @@ function readRejectedProposals(stash, ref) {
|
|
|
142
144
|
/**
|
|
143
145
|
* Synthesize a tmp draft-file path for the agent/sdk file-write contract.
|
|
144
146
|
*
|
|
145
|
-
* Mirrors `src/commands/propose.ts
|
|
146
|
-
* the OpenCode SDK, we instruct the agent to
|
|
147
|
-
* to this file instead of inlining it in
|
|
147
|
+
* Mirrors the draft-path synthesis in `src/commands/proposal/propose.ts` —
|
|
148
|
+
* when the runner is agent-CLI or the OpenCode SDK, we instruct the agent to
|
|
149
|
+
* write the proposal body directly to this file instead of inlining it in
|
|
150
|
+
* JSON on stdout. This bypasses two
|
|
148
151
|
* known failure modes for long assets: (a) ARG_MAX truncation on prompt
|
|
149
152
|
* round-trips through fenced JSON, and (b) embedded-JSON parser brittleness
|
|
150
|
-
* on multi-KB bodies (e.g. the `knowledge
|
|
153
|
+
* on multi-KB bodies (e.g. the `knowledge/systems/KOKORO_USAGE_GUIDE` 8.4KB
|
|
151
154
|
* payload that produced 4/5 `parse_error` in May 2026 reflect validation).
|
|
152
155
|
*
|
|
153
156
|
* The path lives under {@link os.tmpdir} and embeds the (sanitized) ref +
|
|
154
157
|
* timestamp + random suffix so concurrent reflect calls cannot collide.
|
|
155
158
|
*
|
|
156
|
-
*
|
|
157
|
-
* has no filesystem access
|
|
159
|
+
* The LLM HTTP runner cannot use this path because chat-completion transport
|
|
160
|
+
* has no filesystem access.
|
|
158
161
|
*/
|
|
159
162
|
function synthesizeReflectDraftPath(ref) {
|
|
160
163
|
const safeRef = (ref ?? "no-ref").replace(/[^a-z0-9_-]/gi, "_");
|
|
@@ -190,22 +193,27 @@ function hasRelatedSkillSource(content, skillRef) {
|
|
|
190
193
|
const sources = parsed.data.sources;
|
|
191
194
|
return Array.isArray(sources) && sources.some((source) => typeof source === "string" && source.trim() === skillRef);
|
|
192
195
|
}
|
|
193
|
-
async function readRelatedLessons(stash, ref, parsedRef) {
|
|
196
|
+
async function readRelatedLessons(ctx, stash, ref, parsedRef, itemRef) {
|
|
194
197
|
if (parsedRef.type !== "skill")
|
|
195
198
|
return [];
|
|
196
199
|
const related = new Map();
|
|
197
200
|
const derivedLessonRef = deriveLessonRef(ref);
|
|
198
201
|
const candidateRefs = new Set([derivedLessonRef]);
|
|
199
|
-
const derivedLessonPath = path.join(stash, "lessons", `${derivedLessonRef.
|
|
202
|
+
const derivedLessonPath = path.join(stash, "lessons", `${parseRefInput(derivedLessonRef).name}.md`);
|
|
200
203
|
if (fs.existsSync(derivedLessonPath)) {
|
|
201
|
-
|
|
204
|
+
// WI-9.10: genuine content read — routed through the per-invocation asset
|
|
205
|
+
// memo (D6). No write to this same path happens later in this invocation,
|
|
206
|
+
// so memoizing is safe (see run-context.ts's D6 seam docblock).
|
|
207
|
+
related.set(derivedLessonRef, { ref: derivedLessonRef, content: ctx.readAsset(derivedLessonPath) });
|
|
202
208
|
}
|
|
203
209
|
try {
|
|
204
|
-
|
|
210
|
+
// Match events using the candidate's single durable state key.
|
|
211
|
+
const distillInvokedKeys = new Set(improveStateReadRefs(ref, itemRef));
|
|
212
|
+
const feedbackEvents = readEvents({ type: "distill_invoked" }).events.filter((event) => event.ref !== undefined && distillInvokedKeys.has(event.ref));
|
|
205
213
|
for (const event of feedbackEvents) {
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
candidateRefs.add(
|
|
214
|
+
const proposalRef = typeof event.metadata?.proposalRef === "string" ? event.metadata.proposalRef : undefined;
|
|
215
|
+
if (proposalRef && lenientRefType(proposalRef) === "lesson")
|
|
216
|
+
candidateRefs.add(proposalRef);
|
|
209
217
|
}
|
|
210
218
|
}
|
|
211
219
|
catch {
|
|
@@ -213,10 +221,10 @@ async function readRelatedLessons(stash, ref, parsedRef) {
|
|
|
213
221
|
}
|
|
214
222
|
for (const candidateRef of candidateRefs) {
|
|
215
223
|
try {
|
|
216
|
-
const
|
|
217
|
-
if (!
|
|
224
|
+
const filePath = await findAssetFilePath(durableImproveRef(candidateRef), stash);
|
|
225
|
+
if (!filePath || !fs.existsSync(filePath))
|
|
218
226
|
continue;
|
|
219
|
-
const content =
|
|
227
|
+
const content = ctx.readAsset(filePath);
|
|
220
228
|
related.set(candidateRef, { ref: candidateRef, content });
|
|
221
229
|
}
|
|
222
230
|
catch {
|
|
@@ -229,11 +237,11 @@ async function readRelatedLessons(stash, ref, parsedRef) {
|
|
|
229
237
|
for (const fileName of fs.readdirSync(lessonsDir)) {
|
|
230
238
|
if (!fileName.endsWith(".md"))
|
|
231
239
|
continue;
|
|
232
|
-
const content =
|
|
240
|
+
const content = ctx.readAsset(path.join(lessonsDir, fileName));
|
|
233
241
|
if (!hasRelatedSkillSource(content, ref))
|
|
234
242
|
continue;
|
|
235
243
|
const lessonName = fileName.slice(0, -3);
|
|
236
|
-
const lessonRef =
|
|
244
|
+
const lessonRef = conceptIdFromTypeName("lesson", lessonName);
|
|
237
245
|
if (!related.has(lessonRef)) {
|
|
238
246
|
related.set(lessonRef, { ref: lessonRef, content });
|
|
239
247
|
}
|
|
@@ -283,97 +291,38 @@ async function readRelatedLessons(stash, ref, parsedRef) {
|
|
|
283
291
|
/**
|
|
284
292
|
* Returns true only when `stdout` is a recognised AKM proposal-skip signal.
|
|
285
293
|
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
* 2. Legacy text: any line matching `/proposal skipped/i`
|
|
289
|
-
*
|
|
290
|
-
* The previous regex `/cooldown/i` was intentionally broadened to avoid
|
|
291
|
-
* false-positives on real agent error messages that incidentally contain the
|
|
292
|
-
* word "cooldown" (e.g. "rate limit cooldown exceeded"). Only the tightly
|
|
293
|
-
* scoped forms above are treated as legitimate skip signals.
|
|
294
|
+
* Accepted forms are structured JSON: `{ skipped: true }` or
|
|
295
|
+
* `{ reason: "<known-skip-reason>" }`.
|
|
294
296
|
*/
|
|
295
297
|
function isStructuredCooldownSignal(stdout) {
|
|
296
298
|
try {
|
|
297
299
|
const parsed = JSON.parse(stdout.trim());
|
|
298
300
|
if (parsed?.skipped === true)
|
|
299
301
|
return true;
|
|
300
|
-
if (typeof parsed?.reason === "string" &&
|
|
301
|
-
["duplicate_pending", "content_hash_match", "cooldown", "below_threshold"].includes(parsed.reason))
|
|
302
|
+
if (typeof parsed?.reason === "string" && ["fingerprint_match", "rejection_backoff"].includes(parsed.reason))
|
|
302
303
|
return true;
|
|
303
304
|
}
|
|
304
305
|
catch {
|
|
305
306
|
// Non-JSON stdout is never a structured cooldown signal.
|
|
306
307
|
}
|
|
307
|
-
|
|
308
|
-
return /proposal skipped/i.test(stdout);
|
|
308
|
+
return false;
|
|
309
309
|
}
|
|
310
310
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* it requires the content to have a complete proposal structure (frontmatter
|
|
317
|
-
* with required fields or a full heading + body).
|
|
318
|
-
*
|
|
319
|
-
* Strictness rationale: The previous implementation accepted any markdown
|
|
320
|
-
* starting with `#` or `---`, which admitted malformed / hallucinated content
|
|
321
|
-
* as valid proposals. Anthropic agent best practices recommend structured
|
|
322
|
-
* output when the SDK supports it; this tighter fallback is the safety net.
|
|
323
|
-
*
|
|
324
|
-
* When `sdkMode === true`, structured output (tool-call schema) should be used
|
|
325
|
-
* instead of this fallback. That wiring is tracked separately (full SDK
|
|
326
|
-
* structured-output integration); for now this tighter parser applies to all
|
|
327
|
-
* modes and is the primary R-6 deliverable.
|
|
311
|
+
* Best-effort asset type for a maybe-ref string, in the 0.9.0 `[bundle//]conceptId`
|
|
312
|
+
* grammar (`""` when it does not parse). Replaces the pre-0.9.0 `ref.split(":")[0]`
|
|
313
|
+
* type-extraction, which yielded the whole conceptId (`lessons/my-lesson`) instead
|
|
314
|
+
* of the type once refs stopped carrying a `type:` prefix (ref-grammar decision
|
|
315
|
+
* D-R3). Lenient by design — the callers degrade gracefully on an empty type.
|
|
328
316
|
*/
|
|
329
|
-
function
|
|
317
|
+
function lenientRefType(ref) {
|
|
330
318
|
if (!ref)
|
|
331
|
-
return
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return undefined;
|
|
335
|
-
const targetType = ref.split(":")[0];
|
|
336
|
-
if (!looksLikeAssetContent(trimmed, sdkMode, targetType))
|
|
337
|
-
return undefined;
|
|
338
|
-
return { ref, content: trimmed };
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Determine whether raw agent output looks like a valid asset payload (R-6 / #375).
|
|
342
|
-
*
|
|
343
|
-
* Tightened from the previous `startsWith("#") || startsWith("---")`:
|
|
344
|
-
*
|
|
345
|
-
* - YAML frontmatter (`---`): must contain a `description:` field (the only
|
|
346
|
-
* required frontmatter key in v1 spec). This eliminates empty `---\n---\n`
|
|
347
|
-
* blocks and pure delimiter sequences as valid payloads.
|
|
348
|
-
* - Heading start (`#`): must have at least 3 non-blank lines after the heading,
|
|
349
|
-
* to ensure there is actual body content and not just a title stub.
|
|
350
|
-
* - In SDK mode (`sdkMode === true`): additionally requires `when_to_use:` for
|
|
351
|
-
* lesson types (full structured output will replace this in a future PR).
|
|
352
|
-
*/
|
|
353
|
-
function looksLikeAssetContent(value, sdkMode = false, targetType) {
|
|
354
|
-
if (value.startsWith("---")) {
|
|
355
|
-
// YAML frontmatter must contain at least a description field.
|
|
356
|
-
const fmEnd = value.indexOf("\n---", 4);
|
|
357
|
-
if (fmEnd === -1)
|
|
358
|
-
return false;
|
|
359
|
-
const fmBlock = value.slice(0, fmEnd + 4);
|
|
360
|
-
const hasDescription = /^description\s*:/m.test(fmBlock);
|
|
361
|
-
if (!hasDescription)
|
|
362
|
-
return false;
|
|
363
|
-
// In SDK mode, lesson assets additionally require a when_to_use field.
|
|
364
|
-
// Use the target ref type rather than frontmatter type: (which is non-standard).
|
|
365
|
-
if (sdkMode && targetType === "lesson") {
|
|
366
|
-
return /^when_to_use\s*:/m.test(fmBlock);
|
|
367
|
-
}
|
|
368
|
-
return true;
|
|
319
|
+
return "";
|
|
320
|
+
try {
|
|
321
|
+
return parseRefInput(ref).type;
|
|
369
322
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
// This rejects pure title stubs (`# Title\n`) but accepts minimal valid content.
|
|
373
|
-
const lines = value.split("\n").filter((l) => l.trim().length > 0);
|
|
374
|
-
return lines.length >= 2;
|
|
323
|
+
catch {
|
|
324
|
+
return "";
|
|
375
325
|
}
|
|
376
|
-
return false;
|
|
377
326
|
}
|
|
378
327
|
/**
|
|
379
328
|
* Split a markdown blob into `[frontmatterText, bodyText]`.
|
|
@@ -565,7 +514,7 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
|
|
|
565
514
|
// - a present-but-otherwise-invalid description exists (too short, a heading
|
|
566
515
|
// fragment) — overwriting authored content is out of scope; the prompt
|
|
567
516
|
// instruction handles improving it instead.
|
|
568
|
-
const refType =
|
|
517
|
+
const refType = lenientRefType(targetRef);
|
|
569
518
|
const mergedDesc = mergedFm.description;
|
|
570
519
|
const descIsMissing = typeof mergedDesc !== "string" || mergedDesc.trim().length === 0;
|
|
571
520
|
const sourceHadFrontmatter = sourceFmText !== null && Object.keys(sourceFm).length > 0;
|
|
@@ -616,298 +565,936 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
|
|
|
616
565
|
/**
|
|
617
566
|
* JSON Schema for structured reflect output. Passed to `chatCompletion` when
|
|
618
567
|
* the connection has `supportsJsonSchema: true` so the model returns a strict
|
|
619
|
-
* JSON object
|
|
568
|
+
* JSON object containing only the target-scoped fields AKM cannot derive.
|
|
620
569
|
*/
|
|
570
|
+
const REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA = {
|
|
571
|
+
type: "object",
|
|
572
|
+
required: ["description", "when_to_use"],
|
|
573
|
+
additionalProperties: false,
|
|
574
|
+
properties: {
|
|
575
|
+
description: { type: ["string", "null"] },
|
|
576
|
+
when_to_use: { type: ["string", "null"] },
|
|
577
|
+
},
|
|
578
|
+
};
|
|
621
579
|
export const REFLECT_JSON_SCHEMA = {
|
|
622
580
|
type: "object",
|
|
623
|
-
required: ["
|
|
581
|
+
required: ["content", "confidence", "frontmatterPatch"],
|
|
624
582
|
additionalProperties: false,
|
|
625
583
|
properties: {
|
|
626
|
-
|
|
627
|
-
content: { type: "string", description: "Full markdown content for the asset." },
|
|
628
|
-
frontmatter: {
|
|
629
|
-
type: "object",
|
|
630
|
-
description: "Optional frontmatter key-value pairs to merge into the asset.",
|
|
631
|
-
additionalProperties: true,
|
|
632
|
-
},
|
|
584
|
+
content: { type: "string", description: "Complete improved markdown body without YAML frontmatter." },
|
|
633
585
|
// Phase 6A (Advantage D6a): self-reported confidence in [0, 1]. When the
|
|
634
586
|
// LLM is well-calibrated, scores at or above the configured threshold
|
|
635
587
|
// (default 0.8) drive auto-accept in `akm improve`. Out-of-range or
|
|
636
|
-
// non-finite values are
|
|
637
|
-
//
|
|
588
|
+
// non-finite values are rejected by direct-output extraction. Agent and SDK
|
|
589
|
+
// confidence remains optional on their separate existing contracts.
|
|
590
|
+
confidence: {
|
|
591
|
+
type: "number",
|
|
592
|
+
minimum: 0,
|
|
593
|
+
maximum: 1,
|
|
594
|
+
description: "Self-reported quality confidence in [0, 1]. Persisted on the proposal for reviewers and the triage judge to read during adjudication.",
|
|
595
|
+
},
|
|
596
|
+
frontmatterPatch: REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA,
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
const REFLECT_UNSCOPED_JSON_SCHEMA = {
|
|
600
|
+
type: "object",
|
|
601
|
+
required: ["ref", "content", "confidence", "frontmatterPatch"],
|
|
602
|
+
additionalProperties: false,
|
|
603
|
+
properties: {
|
|
604
|
+
ref: { type: "string", description: "Selected asset ref as a subdir-qualified conceptId." },
|
|
605
|
+
content: { type: "string", description: "Complete improved markdown body without YAML frontmatter." },
|
|
638
606
|
confidence: {
|
|
639
607
|
type: "number",
|
|
640
608
|
minimum: 0,
|
|
641
609
|
maximum: 1,
|
|
642
|
-
description: "
|
|
610
|
+
description: "Self-reported quality confidence in [0, 1].",
|
|
643
611
|
},
|
|
612
|
+
frontmatterPatch: REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA,
|
|
644
613
|
},
|
|
645
614
|
};
|
|
646
615
|
/** Critique prompt injected between prior draft and refinement request (Self-Refine loop). */
|
|
647
|
-
const REFLECT_CRITIQUE_PROMPT = "Your previous proposal is shown above.
|
|
616
|
+
const REFLECT_CRITIQUE_PROMPT = "Your previous proposal is shown above. Review it critically and provide an improved version that is more specific, actionable, and avoids any issues with the previous attempt. Return only the improved response using the output contract from the original prompt.";
|
|
617
|
+
function reflectLlmTelemetry(result) {
|
|
618
|
+
if (!result.parsed || typeof result.parsed !== "object" || Array.isArray(result.parsed))
|
|
619
|
+
return undefined;
|
|
620
|
+
const parsed = result.parsed;
|
|
621
|
+
if (parsed.outputMode !== "json_schema" && parsed.outputMode !== "framed_markdown")
|
|
622
|
+
return undefined;
|
|
623
|
+
if (typeof parsed.repairAttempts !== "number")
|
|
624
|
+
return undefined;
|
|
625
|
+
return { outputMode: parsed.outputMode, repairAttempts: parsed.repairAttempts };
|
|
626
|
+
}
|
|
627
|
+
function reflectLlmPriorDraft(result) {
|
|
628
|
+
if (!result.parsed || typeof result.parsed !== "object" || Array.isArray(result.parsed))
|
|
629
|
+
return undefined;
|
|
630
|
+
const priorDraft = result.parsed.priorDraft;
|
|
631
|
+
return typeof priorDraft === "string" ? priorDraft : undefined;
|
|
632
|
+
}
|
|
633
|
+
function parseReflectConfidence(value) {
|
|
634
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
635
|
+
throw new Error('direct reflect response missing required number field "confidence" in [0, 1]');
|
|
636
|
+
}
|
|
637
|
+
return value;
|
|
638
|
+
}
|
|
639
|
+
function parseReflectFrontmatterPatch(value) {
|
|
640
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
641
|
+
throw new Error('direct reflect response missing required object field "frontmatterPatch"');
|
|
642
|
+
}
|
|
643
|
+
const patch = value;
|
|
644
|
+
const keys = Object.keys(patch).sort();
|
|
645
|
+
if (keys.length !== 2 || keys[0] !== "description" || keys[1] !== "when_to_use") {
|
|
646
|
+
throw new Error("direct reflect frontmatterPatch fields must be exactly: description, when_to_use");
|
|
647
|
+
}
|
|
648
|
+
const frontmatter = {};
|
|
649
|
+
for (const field of ["description", "when_to_use"]) {
|
|
650
|
+
const fieldValue = patch[field];
|
|
651
|
+
if (fieldValue === null)
|
|
652
|
+
continue;
|
|
653
|
+
if (typeof fieldValue !== "string" || !fieldValue.trim() || /[\r\n]/.test(fieldValue)) {
|
|
654
|
+
throw new Error(`direct reflect frontmatterPatch.${field} must be a non-empty single-line string or null`);
|
|
655
|
+
}
|
|
656
|
+
frontmatter[field] = fieldValue.trim();
|
|
657
|
+
}
|
|
658
|
+
return Object.keys(frontmatter).length > 0 ? frontmatter : undefined;
|
|
659
|
+
}
|
|
660
|
+
function parseSchemaReflectOutput(raw, targetRef) {
|
|
661
|
+
const parsed = parseEmbeddedJsonResponse(raw);
|
|
662
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
663
|
+
throw new Error("direct reflect response was not valid JSON");
|
|
664
|
+
}
|
|
665
|
+
const expectedKeys = targetRef
|
|
666
|
+
? ["confidence", "content", "frontmatterPatch"]
|
|
667
|
+
: ["confidence", "content", "frontmatterPatch", "ref"];
|
|
668
|
+
const actualKeys = Object.keys(parsed).sort();
|
|
669
|
+
if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, index) => key !== expectedKeys[index])) {
|
|
670
|
+
throw new Error(`direct reflect response fields must be exactly: ${expectedKeys.join(", ")}`);
|
|
671
|
+
}
|
|
672
|
+
if (typeof parsed.content !== "string" || !parsed.content.trim()) {
|
|
673
|
+
throw new Error('direct reflect response missing required string field "content"');
|
|
674
|
+
}
|
|
675
|
+
const ref = targetRef ?? (typeof parsed.ref === "string" ? parsed.ref.trim() : "");
|
|
676
|
+
if (!ref)
|
|
677
|
+
throw new Error('direct reflect response missing required string field "ref"');
|
|
678
|
+
const frontmatter = parseReflectFrontmatterPatch(parsed.frontmatterPatch);
|
|
679
|
+
return {
|
|
680
|
+
ref,
|
|
681
|
+
content: parsed.content,
|
|
682
|
+
confidence: parseReflectConfidence(parsed.confidence),
|
|
683
|
+
...(frontmatter ? { frontmatter } : {}),
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function parseFramedReflectOutput(raw, targetRef) {
|
|
687
|
+
const normalized = raw.replaceAll("\r\n", "\n").trim();
|
|
688
|
+
const beginMarker = "AKM_REFLECT_CONTENT_BEGIN\n";
|
|
689
|
+
const endMarker = "\nAKM_REFLECT_CONTENT_END";
|
|
690
|
+
const beginIndex = normalized.indexOf(beginMarker);
|
|
691
|
+
if (beginIndex < 0 || (beginIndex > 0 && normalized[beginIndex - 1] !== "\n")) {
|
|
692
|
+
throw new Error("direct reflect response missing AKM_REFLECT_CONTENT_BEGIN marker");
|
|
693
|
+
}
|
|
694
|
+
const contentStart = beginIndex + beginMarker.length;
|
|
695
|
+
const endIndex = normalized.lastIndexOf(endMarker);
|
|
696
|
+
if (endIndex < contentStart || normalized.slice(endIndex + endMarker.length).trim()) {
|
|
697
|
+
throw new Error("direct reflect response missing terminal AKM_REFLECT_CONTENT_END marker");
|
|
698
|
+
}
|
|
699
|
+
const headerLines = normalized.slice(0, beginIndex).trim().split("\n").filter(Boolean);
|
|
700
|
+
const confidenceLine = headerLines.find((line) => line.startsWith("AKM_REFLECT_CONFIDENCE:"));
|
|
701
|
+
const refLine = headerLines.find((line) => line.startsWith("AKM_REFLECT_REF:"));
|
|
702
|
+
const patchLine = headerLines.find((line) => line.startsWith("AKM_REFLECT_FRONTMATTER_PATCH:"));
|
|
703
|
+
const expectedHeaderLines = targetRef ? 2 : 3;
|
|
704
|
+
const invalidRefLine = targetRef ? refLine !== undefined : refLine === undefined;
|
|
705
|
+
if (headerLines.length !== expectedHeaderLines || !confidenceLine || !patchLine || invalidRefLine) {
|
|
706
|
+
throw new Error("direct reflect response contained invalid frame metadata");
|
|
707
|
+
}
|
|
708
|
+
const confidenceText = confidenceLine.slice("AKM_REFLECT_CONFIDENCE:".length).trim();
|
|
709
|
+
if (!/^(?:0(?:\.\d+)?|1(?:\.0+)?)$/.test(confidenceText)) {
|
|
710
|
+
throw new Error("direct reflect frame confidence must be a decimal number in [0, 1]");
|
|
711
|
+
}
|
|
712
|
+
const confidence = parseReflectConfidence(Number(confidenceText));
|
|
713
|
+
const ref = targetRef ?? refLine?.slice("AKM_REFLECT_REF:".length).trim() ?? "";
|
|
714
|
+
if (!ref)
|
|
715
|
+
throw new Error("direct reflect response contained an empty AKM_REFLECT_REF value");
|
|
716
|
+
const content = normalized.slice(contentStart, endIndex);
|
|
717
|
+
if (!content.trim())
|
|
718
|
+
throw new Error("direct reflect response contained empty framed content");
|
|
719
|
+
const patchText = patchLine.slice("AKM_REFLECT_FRONTMATTER_PATCH:".length).trim();
|
|
720
|
+
let parsedPatch;
|
|
721
|
+
try {
|
|
722
|
+
parsedPatch = JSON.parse(patchText);
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
throw new Error("direct reflect response contained invalid frontmatter patch JSON");
|
|
726
|
+
}
|
|
727
|
+
const frontmatter = parseReflectFrontmatterPatch(parsedPatch);
|
|
728
|
+
return { ref, content, confidence, ...(frontmatter ? { frontmatter } : {}) };
|
|
729
|
+
}
|
|
730
|
+
function parseDirectReflectOutput(raw, mode, targetRef) {
|
|
731
|
+
return mode === "json_schema" ? parseSchemaReflectOutput(raw, targetRef) : parseFramedReflectOutput(raw, targetRef);
|
|
732
|
+
}
|
|
648
733
|
/**
|
|
649
734
|
* Run a single reflect iteration directly via the LLM API (v2 config path).
|
|
650
735
|
*
|
|
651
736
|
* Returns an {@link AgentRunResult}-shaped object so it can slot into the same
|
|
652
|
-
* dispatch loop as agent-based runners.
|
|
653
|
-
* LLM
|
|
654
|
-
* into the result rather than thrown.
|
|
737
|
+
* dispatch loop as agent-based runners. Production calls extract the selected
|
|
738
|
+
* direct-LLM contract and normalize it to proposal JSON in `stdout`. Errors
|
|
739
|
+
* are captured into the result rather than thrown.
|
|
655
740
|
*/
|
|
656
741
|
export async function runReflectViaLlm(opts) {
|
|
657
742
|
const start = Date.now();
|
|
743
|
+
let repairAttempts = 0;
|
|
658
744
|
const messages = [{ role: "user", content: opts.prompt ?? "" }];
|
|
745
|
+
const configuredTimeout = Object.hasOwn(opts, "timeoutMs")
|
|
746
|
+
? (opts.timeoutMs ?? null)
|
|
747
|
+
: Object.hasOwn(opts.connection, "timeoutMs")
|
|
748
|
+
? (opts.connection.timeoutMs ?? null)
|
|
749
|
+
: DEFAULT_LLM_TIMEOUT_MS;
|
|
750
|
+
const deadline = typeof configuredTimeout === "number" ? start + configuredTimeout : undefined;
|
|
659
751
|
if (opts.priorDraft !== undefined && opts.iteration > 0) {
|
|
660
752
|
messages.push({ role: "assistant", content: opts.priorDraft });
|
|
661
753
|
messages.push({ role: "user", content: REFLECT_CRITIQUE_PROMPT });
|
|
662
754
|
}
|
|
755
|
+
const call = async (callMessages, repairTimeoutMs) => callStructured({
|
|
756
|
+
feature: "reflect_proposal",
|
|
757
|
+
config: opts.connection,
|
|
758
|
+
messages: callMessages,
|
|
759
|
+
request: {
|
|
760
|
+
...(repairTimeoutMs !== undefined
|
|
761
|
+
? { timeoutMs: repairTimeoutMs }
|
|
762
|
+
: Object.hasOwn(opts, "timeoutMs")
|
|
763
|
+
? { timeoutMs: opts.timeoutMs }
|
|
764
|
+
: {}),
|
|
765
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
766
|
+
...(opts.responseSchema !== undefined ? { responseSchema: opts.responseSchema } : {}),
|
|
767
|
+
...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
|
|
768
|
+
// Reflect requires a machine-readable payload. Visible chain-of-thought
|
|
769
|
+
// can consume the output cap before the model reaches the envelope.
|
|
770
|
+
enableThinking: false,
|
|
771
|
+
...(opts.chat ? { chat: opts.chat } : {}),
|
|
772
|
+
},
|
|
773
|
+
parse: (raw) => raw ?? "",
|
|
774
|
+
// Unreachable on the ungated path (errors propagate to the catch below).
|
|
775
|
+
onError: () => "",
|
|
776
|
+
fallback: "",
|
|
777
|
+
});
|
|
778
|
+
const failure = (err, reason, repairAttempts, stdout = "", exitCode = 1) => {
|
|
779
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
780
|
+
return {
|
|
781
|
+
ok: false,
|
|
782
|
+
stdout,
|
|
783
|
+
stderr: msg,
|
|
784
|
+
durationMs: Date.now() - start,
|
|
785
|
+
exitCode,
|
|
786
|
+
reason,
|
|
787
|
+
error: msg,
|
|
788
|
+
parsed: { outputMode: opts.outputMode, repairAttempts },
|
|
789
|
+
};
|
|
790
|
+
};
|
|
663
791
|
try {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
792
|
+
if (opts.signal?.aborted)
|
|
793
|
+
throw new Error("Reflect request aborted");
|
|
794
|
+
const stdout = await call(messages);
|
|
795
|
+
let payload;
|
|
796
|
+
let acceptedOutput = stdout;
|
|
797
|
+
try {
|
|
798
|
+
payload = parseDirectReflectOutput(stdout, opts.outputMode, opts.targetRef);
|
|
668
799
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
800
|
+
catch (err) {
|
|
801
|
+
if (opts.allowRepair === false)
|
|
802
|
+
return failure(err, "parse_error", 0, stdout, 0);
|
|
803
|
+
if (opts.signal?.aborted)
|
|
804
|
+
return failure(new Error("Reflect request aborted"), "aborted", 0, stdout);
|
|
805
|
+
const remaining = deadline === undefined ? undefined : deadline - Date.now();
|
|
806
|
+
if (remaining !== undefined && remaining <= 0) {
|
|
807
|
+
return failure(new LlmCallError("Reflect request timed out before output repair", "timeout"), "timeout", 0, stdout);
|
|
808
|
+
}
|
|
809
|
+
repairAttempts = 1;
|
|
810
|
+
const repairMessages = [
|
|
811
|
+
...messages,
|
|
812
|
+
{ role: "assistant", content: stdout },
|
|
813
|
+
{
|
|
814
|
+
role: "user",
|
|
815
|
+
content: buildReflectOutputRepairPrompt(opts.outputMode, opts.targetRef !== undefined),
|
|
816
|
+
},
|
|
817
|
+
];
|
|
818
|
+
const repaired = await call(repairMessages, remaining);
|
|
819
|
+
acceptedOutput = repaired;
|
|
820
|
+
try {
|
|
821
|
+
payload = parseDirectReflectOutput(repaired, opts.outputMode, opts.targetRef);
|
|
822
|
+
}
|
|
823
|
+
catch (err) {
|
|
824
|
+
return failure(err, "parse_error", repairAttempts, repaired, 0);
|
|
825
|
+
}
|
|
676
826
|
}
|
|
677
827
|
return {
|
|
678
828
|
ok: true,
|
|
679
|
-
stdout,
|
|
829
|
+
stdout: JSON.stringify(payload),
|
|
680
830
|
stderr: "",
|
|
681
831
|
durationMs: Date.now() - start,
|
|
682
832
|
exitCode: 0,
|
|
833
|
+
parsed: { outputMode: opts.outputMode, repairAttempts, priorDraft: acceptedOutput },
|
|
683
834
|
};
|
|
684
835
|
}
|
|
685
836
|
catch (err) {
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
exitCode: 1,
|
|
693
|
-
reason: "non_zero_exit",
|
|
694
|
-
error: msg,
|
|
695
|
-
};
|
|
837
|
+
const reason = opts.signal?.aborted
|
|
838
|
+
? "aborted"
|
|
839
|
+
: err instanceof LlmCallError && err.code === "timeout"
|
|
840
|
+
? "timeout"
|
|
841
|
+
: "non_zero_exit";
|
|
842
|
+
return failure(err, reason, repairAttempts);
|
|
696
843
|
}
|
|
697
844
|
}
|
|
698
|
-
function failureEnvelope(result, ref, fallbackReason = "non_zero_exit") {
|
|
845
|
+
function failureEnvelope(result, ref, engine, fallbackReason = "non_zero_exit") {
|
|
699
846
|
return {
|
|
700
847
|
...baseFailureFields(result, fallbackReason),
|
|
848
|
+
schemaVersion: 2,
|
|
701
849
|
...(ref ? { ref } : {}),
|
|
850
|
+
...(engine ? { engine } : {}),
|
|
702
851
|
};
|
|
703
852
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const emitReflectFailed = (reason, subreason, ref, extra) => {
|
|
853
|
+
/**
|
|
854
|
+
* Reflect content-preservation + proposal creation: restore/reset protected
|
|
855
|
+
* frontmatter and reject unsafe body-size ratios (sanitizeReflectPayload), the
|
|
856
|
+
* #580 noise gate, the optional quality judge, then create the proposal (with
|
|
857
|
+
* the R-4/#373 lesson provenance stamp) and emit `reflect_completed`. Extracted
|
|
858
|
+
* verbatim from `akmReflect`; every reject/skip envelope and event is
|
|
859
|
+
* byte-identical.
|
|
860
|
+
*/
|
|
861
|
+
async function finalizeReflectProposal(args) {
|
|
862
|
+
const { assetContent, result, options, engineName, config, activeStrategy, runnerSpec, feedback, stash, emitReflectFailed, } = args;
|
|
863
|
+
let payload = args.payload;
|
|
864
|
+
const outputTelemetry = reflectLlmTelemetry(result);
|
|
865
|
+
// 7. Reflect content-preservation rails:
|
|
866
|
+
// - Restore source frontmatter so reflect can never strip indexable
|
|
867
|
+
// fields (`description`, `when_to_use`, `tags`, ...).
|
|
868
|
+
// - Reset protected identity fields (`name`, `ref`, `id`, `slug`,
|
|
869
|
+
// `type`) the LLM tried to change.
|
|
870
|
+
// - Reject proposals that shrink/expand the body past safe ratios.
|
|
871
|
+
//
|
|
872
|
+
// See REFLECT_ALLOWED_TYPES / sanitizeReflectPayload for the underlying
|
|
873
|
+
// hypotheses + observed regressions (`8737ab63`, `26941510`, and the
|
|
874
|
+
// catastrophic-shrinkage cases from the May 2026 review).
|
|
875
|
+
const sanitizeOutcome = sanitizeReflectPayload({ content: payload.content, ...(payload.frontmatter ? { frontmatter: payload.frontmatter } : {}) }, assetContent, payload.ref);
|
|
876
|
+
if (sanitizeOutcome.reject) {
|
|
729
877
|
appendEvent({
|
|
730
878
|
eventType: "reflect_completed",
|
|
731
|
-
|
|
879
|
+
ref: payload.ref,
|
|
732
880
|
metadata: {
|
|
733
881
|
source: "reflect",
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
...(
|
|
882
|
+
sanitized: true,
|
|
883
|
+
rejected: true,
|
|
884
|
+
rejectReason: sanitizeOutcome.reject.error,
|
|
885
|
+
...(sanitizeOutcome.warnings.length > 0 ? { sanitizerWarnings: sanitizeOutcome.warnings } : {}),
|
|
886
|
+
...(outputTelemetry ?? {}),
|
|
738
887
|
},
|
|
739
|
-
});
|
|
888
|
+
}, options.eventsCtx);
|
|
889
|
+
return {
|
|
890
|
+
schemaVersion: 2,
|
|
891
|
+
ok: false,
|
|
892
|
+
reason: sanitizeOutcome.reject.reason,
|
|
893
|
+
error: sanitizeOutcome.reject.error,
|
|
894
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
895
|
+
engine: engineName,
|
|
896
|
+
exitCode: result.exitCode,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
payload = {
|
|
900
|
+
...payload,
|
|
901
|
+
content: sanitizeOutcome.content,
|
|
902
|
+
...(sanitizeOutcome.frontmatter ? { frontmatter: sanitizeOutcome.frontmatter } : {}),
|
|
740
903
|
};
|
|
741
|
-
//
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
904
|
+
// 7c. Noise gate (#580): never queue a proposal whose sanitized content is
|
|
905
|
+
// identical to the current asset (empty diff) or differs only cosmetically
|
|
906
|
+
// (whitespace reflow, code-fence language hints, YAML scalar re-folding).
|
|
907
|
+
// Pure deterministic text comparison — see `reflect-noise.ts`. Skipped when
|
|
908
|
+
// there is no source asset (new-asset proposals have nothing to diff against).
|
|
909
|
+
if (assetContent !== undefined) {
|
|
910
|
+
const changeKind = classifyReflectChange(assetContent, payload.content);
|
|
911
|
+
// 'low-value' is config-gated (#639). DEFAULT OFF — absent = byte-identical
|
|
912
|
+
// pre-#639 behaviour (low-value treated the same as substantive). Resolved
|
|
913
|
+
// by the caller from the active improve strategy's
|
|
914
|
+
// `processes.reflect.lowValueFilter.enabled` and passed via options, so the
|
|
915
|
+
// running strategy decides.
|
|
916
|
+
const lowValueFilterEnabled = options.lowValueFilter === true;
|
|
917
|
+
const isDeferred = changeKind === "noop" || changeKind === "cosmetic" || (changeKind === "low-value" && lowValueFilterEnabled);
|
|
918
|
+
if (isDeferred) {
|
|
919
|
+
const subreason = changeKind === "noop"
|
|
920
|
+
? "reflect_skipped_noop"
|
|
921
|
+
: changeKind === "low-value"
|
|
922
|
+
? "reflect_skipped_low_value"
|
|
923
|
+
: "reflect_skipped_cosmetic";
|
|
924
|
+
emitReflectFailed("no_change", subreason, options.ref, { changeKind, ...(outputTelemetry ?? {}) });
|
|
757
925
|
return {
|
|
758
|
-
schemaVersion:
|
|
926
|
+
schemaVersion: 2,
|
|
759
927
|
ok: false,
|
|
760
|
-
reason: "
|
|
761
|
-
error:
|
|
762
|
-
|
|
763
|
-
|
|
928
|
+
reason: "no_change",
|
|
929
|
+
error: changeKind === "noop"
|
|
930
|
+
? `Reflect skipped: proposed content for ${payload.ref} is identical to the current asset (empty diff); no proposal created.`
|
|
931
|
+
: changeKind === "low-value"
|
|
932
|
+
? `Reflect skipped: proposed content for ${payload.ref} is a low-value prose micro-rewrite (few changed tokens, no structural changes); no proposal created.`
|
|
933
|
+
: `Reflect skipped: proposed content for ${payload.ref} is a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes); no proposal created.`,
|
|
934
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
935
|
+
engine: engineName,
|
|
936
|
+
exitCode: result.exitCode,
|
|
764
937
|
};
|
|
765
938
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
939
|
+
}
|
|
940
|
+
// 7c. Judge the exact sanitized content that can be persisted. Fail closed
|
|
941
|
+
// on cancellation, transport failure, malformed output, or an invalid score.
|
|
942
|
+
const qualityGateEnabled = (activeStrategy?.processes?.reflect?.qualityGate?.enabled ?? false) ||
|
|
943
|
+
(activeStrategy?.processes?.distill?.qualityGate?.enabled ?? true);
|
|
944
|
+
if (qualityGateEnabled) {
|
|
945
|
+
const judgeResult = await runReflectQualityJudge(config, payload.content, assetContent ?? "", feedback, options.chat, {
|
|
946
|
+
...(runnerIsLlm(runnerSpec) ? { llmConfig: materializeLlmRunnerConnection(runnerSpec) } : {}),
|
|
947
|
+
...(Object.hasOwn(options, "timeoutMs") ? { timeoutMs: options.timeoutMs } : {}),
|
|
948
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
949
|
+
});
|
|
950
|
+
if (!judgeResult.pass) {
|
|
951
|
+
appendEvent({
|
|
952
|
+
eventType: "reflect_completed",
|
|
953
|
+
ref: payload.ref,
|
|
954
|
+
metadata: {
|
|
955
|
+
source: "reflect",
|
|
956
|
+
qualityRejected: true,
|
|
957
|
+
qualityScore: judgeResult.score,
|
|
958
|
+
qualityReason: judgeResult.reason,
|
|
959
|
+
...(outputTelemetry ?? {}),
|
|
960
|
+
},
|
|
961
|
+
}, options.eventsCtx);
|
|
962
|
+
return {
|
|
963
|
+
schemaVersion: 2,
|
|
964
|
+
ok: false,
|
|
965
|
+
reason: "parse_error",
|
|
966
|
+
error: `Reflect proposal quality gate rejected: score=${judgeResult.score}, reason="${judgeResult.reason}"`,
|
|
967
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
968
|
+
engine: engineName,
|
|
969
|
+
exitCode: result.exitCode,
|
|
970
|
+
};
|
|
780
971
|
}
|
|
781
972
|
}
|
|
782
|
-
|
|
783
|
-
|
|
973
|
+
return createReflectProposal({
|
|
974
|
+
payload,
|
|
975
|
+
options,
|
|
976
|
+
stash,
|
|
977
|
+
engineName,
|
|
978
|
+
durationMs: result.durationMs,
|
|
979
|
+
emitReflectFailed,
|
|
980
|
+
outputTelemetry,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Create the reflect proposal from sanitized+judged payload: stamp the R-4/#373
|
|
985
|
+
* lesson provenance marker, call `createProposal`, and emit the terminal
|
|
986
|
+
* `reflect_completed` (or a cooldown skip envelope). Extracted verbatim from
|
|
987
|
+
* `akmReflect`'s finalize tail.
|
|
988
|
+
*/
|
|
989
|
+
function createReflectProposal(args) {
|
|
990
|
+
const { payload, options, stash, engineName, durationMs, emitReflectFailed, outputTelemetry } = args;
|
|
991
|
+
// 8. Create the proposal. The proposal queue is the ONLY thing reflect
|
|
992
|
+
// writes — promotion to a real asset is gated by `akm proposal accept`.
|
|
784
993
|
//
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
profile = options.agentProfile;
|
|
795
|
-
}
|
|
796
|
-
else if (options.runner) {
|
|
797
|
-
// Caller-provided RunnerSpec (used in tests and --dry-run-resolve).
|
|
798
|
-
runnerSpec = options.runner;
|
|
799
|
-
}
|
|
800
|
-
else {
|
|
801
|
-
const cfg = options.config ?? loadConfig();
|
|
802
|
-
const reflectProcess = getImproveProcessConfig(cfg, "reflect", options.improveProfile);
|
|
803
|
-
// Resolve the runner from the improve profile's reflect entry when present.
|
|
804
|
-
runnerSpec = resolveImproveProcessRunnerFromProfile(reflectProcess, cfg) ?? undefined;
|
|
805
|
-
if (runnerSpec) {
|
|
806
|
-
if (resolvedTimeoutMs === undefined && runnerSpec.timeoutMs !== undefined) {
|
|
807
|
-
resolvedTimeoutMs = runnerSpec.timeoutMs;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
if (options.profile) {
|
|
812
|
-
// Explicit --profile flag wins over process config.
|
|
813
|
-
profile = resolveAgentProfile(options);
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
// Use per-process config resolution (falls back to defaults.agent).
|
|
817
|
-
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
818
|
-
const processName = options.agentProcess ?? "reflect";
|
|
819
|
-
const resolved = resolveProcessAgentProfile(processName, agent);
|
|
820
|
-
profile = resolved.profile;
|
|
821
|
-
// Only apply process-resolved timeoutMs when caller didn't supply one.
|
|
822
|
-
if (resolvedTimeoutMs === undefined) {
|
|
823
|
-
resolvedTimeoutMs = resolved.timeoutMs;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
994
|
+
// R-4 / #373: Stamp `derived_from_reflect: true` in the frontmatter of any
|
|
995
|
+
// lesson proposal generated by reflect. This provenance marker lets
|
|
996
|
+
// `readRelatedLessons` exclude echo-chamber lessons (lessons that originate
|
|
997
|
+
// from prior reflect runs on the same skill) unless independent feedback
|
|
998
|
+
// evidence exists. ExpeL arXiv:2308.10144 — reject rules without success/
|
|
999
|
+
// failure differential from independent evidence.
|
|
1000
|
+
const isLessonProposal = (() => {
|
|
1001
|
+
try {
|
|
1002
|
+
return parseRefInput(payload.ref).type === "lesson";
|
|
827
1003
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
if (err instanceof ConfigError || err instanceof UsageError)
|
|
831
|
-
throw err;
|
|
832
|
-
throw err;
|
|
833
|
-
}
|
|
834
|
-
// P1.3 (meta-review 07, Chain G): unattended `akm improve` must never hand
|
|
835
|
-
// reflect a tool-capable runner. Agent-CLI/SDK runners have filesystem
|
|
836
|
-
// access, so a poisoned asset body flowing through the scheduled reflect
|
|
837
|
-
// prompt could steer an agent with tools; the tool-less LLM HTTP runner
|
|
838
|
-
// corrupts output text at worst (src/llm/call-ai.ts). An llm RunnerSpec
|
|
839
|
-
// resolved from the process block is honored; anything else — an agent/sdk
|
|
840
|
-
// spec, a resolved agent profile, or the default-agent fallback below — is
|
|
841
|
-
// pinned to `defaults.llm`, failing CLOSED when that is unset.
|
|
842
|
-
// `options.agentProfile` stays exempt: it is a direct-injection test seam
|
|
843
|
-
// the cron path (loop-stages) never sets.
|
|
844
|
-
if (options.eventSource === "improve" && !options.agentProfile && (!runnerSpec || !runnerIsLlm(runnerSpec))) {
|
|
845
|
-
const cfg = options.config ?? loadConfig();
|
|
846
|
-
const pinned = resolveDefaultLlmRunner(cfg, resolvedTimeoutMs);
|
|
847
|
-
if (!pinned) {
|
|
848
|
-
throw new ConfigError("Unattended improve pins reflect to the tool-less LLM runner, but the config resolves a tool-capable runner and no defaults.llm profile exists to pin to.", "LLM_NOT_CONFIGURED", 'Set processes.reflect.mode to "llm" on the active improve profile, or configure defaults.llm.');
|
|
1004
|
+
catch {
|
|
1005
|
+
return false;
|
|
849
1006
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1007
|
+
})();
|
|
1008
|
+
const basePayloadFrontmatter = payload.frontmatter ?? {};
|
|
1009
|
+
const payloadFrontmatterWithProvenance = isLessonProposal
|
|
1010
|
+
? { ...basePayloadFrontmatter, derived_from_reflect: true }
|
|
1011
|
+
: basePayloadFrontmatter;
|
|
1012
|
+
const createInput = {
|
|
1013
|
+
ref: payload.ref,
|
|
1014
|
+
...(options.target ? { target: options.target } : {}),
|
|
1015
|
+
source: "reflect",
|
|
1016
|
+
sourceRun: `reflect-${Date.now()}`,
|
|
1017
|
+
payload: {
|
|
1018
|
+
content: payload.content,
|
|
1019
|
+
...(Object.keys(payloadFrontmatterWithProvenance).length > 0
|
|
1020
|
+
? { frontmatter: payloadFrontmatterWithProvenance }
|
|
1021
|
+
: {}),
|
|
1022
|
+
},
|
|
1023
|
+
// Phase 6A: forward LLM-reported confidence into the proposal record.
|
|
1024
|
+
// `parseAgentProposalPayload` already clamps to [0, 1] and drops non-
|
|
1025
|
+
// finite values; `createProposal` runs its own sanitizer as a safety net.
|
|
1026
|
+
...(typeof payload.confidence === "number" ? { confidence: payload.confidence } : {}),
|
|
1027
|
+
// Attribution tagging: persist the eligibility lane on the proposal so it
|
|
1028
|
+
// survives to accept/reject/revert time even across runs. See EligibilitySource.
|
|
1029
|
+
...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
|
|
1030
|
+
// §23.6 fingerprint model-id term (WI-6.4): the engine that generated
|
|
1031
|
+
// this draft (reflect resolves engines, not bare model ids).
|
|
1032
|
+
modelId: engineName,
|
|
1033
|
+
};
|
|
1034
|
+
const proposalResult = emitProposal({ stashDir: stash, proposalsCtx: options.ctx }, createInput);
|
|
1035
|
+
if (isProposalSkipped(proposalResult)) {
|
|
1036
|
+
// Dedup/cooldown guard fired — surface as a "cooldown" reason (not "parse_error")
|
|
1037
|
+
// so the improve orchestrator can distinguish legitimate skips from real failures
|
|
1038
|
+
// and exclude them from recentErrors/avoidPatterns injection.
|
|
1039
|
+
emitReflectFailed("cooldown", "proposal_skipped", options.ref, {
|
|
1040
|
+
proposalSkipReason: proposalResult.reason,
|
|
1041
|
+
...(outputTelemetry ?? {}),
|
|
1042
|
+
});
|
|
1043
|
+
return {
|
|
1044
|
+
schemaVersion: 2,
|
|
1045
|
+
ok: false,
|
|
1046
|
+
reason: "cooldown",
|
|
1047
|
+
error: `Proposal skipped (${proposalResult.reason}): ${proposalResult.message}`,
|
|
1048
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
1049
|
+
engine: engineName,
|
|
1050
|
+
exitCode: null,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
const proposal = proposalResult;
|
|
1054
|
+
appendEvent({
|
|
1055
|
+
eventType: "reflect_completed",
|
|
1056
|
+
ref: proposal.ref,
|
|
1057
|
+
metadata: {
|
|
1058
|
+
proposalId: proposal.id,
|
|
1059
|
+
source: "reflect",
|
|
1060
|
+
engine: engineName,
|
|
1061
|
+
...(outputTelemetry ?? {}),
|
|
1062
|
+
},
|
|
1063
|
+
}, options.eventsCtx);
|
|
1064
|
+
return {
|
|
1065
|
+
schemaVersion: 2,
|
|
1066
|
+
ok: true,
|
|
1067
|
+
proposal,
|
|
1068
|
+
ref: proposal.ref,
|
|
1069
|
+
engine: engineName,
|
|
1070
|
+
durationMs,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Resolve the agent's proposal payload from a successful run: the file-write
|
|
1075
|
+
* contract path (read `lastDraftPath`, extract self-rated confidence) or the
|
|
1076
|
+
* JSON-stdout path used by direct LLM runners. Returns the payload or a terminal
|
|
1077
|
+
* failure envelope.
|
|
1078
|
+
*/
|
|
1079
|
+
function resolveReflectPayload(args) {
|
|
1080
|
+
const { result, lastDraftPath, sensitiveValues, options, engineName, emitReflectFailed } = args;
|
|
1081
|
+
// 6. Resolve the proposal content.
|
|
1082
|
+
//
|
|
1083
|
+
// Path A (file-write contract — preferred for agent/sdk runners on long
|
|
1084
|
+
// assets): the agent wrote the body to `lastDraftPath` and printed
|
|
1085
|
+
// `DRAFT_WRITTEN` on stdout. Load the body from disk and synthesize a
|
|
1086
|
+
// payload. The `EXCESSIVE_EXPANSION`/schema-shape gates downstream still
|
|
1087
|
+
// apply — they validate content, not transport.
|
|
1088
|
+
//
|
|
1089
|
+
// Path B (JSON stdout): the direct LLM runner cannot honour file-write.
|
|
1090
|
+
const draftFileExists = lastDraftPath !== undefined && fs.existsSync(lastDraftPath) && fs.statSync(lastDraftPath).size > 0;
|
|
1091
|
+
const draftSignaled = stdoutSignalsDraftWritten(result.stdout);
|
|
1092
|
+
if (draftSignaled && lastDraftPath && !draftFileExists) {
|
|
1093
|
+
// Agent claimed to write the draft but the file is missing or empty.
|
|
1094
|
+
// Surface as a parse_error rather than silently falling through — the
|
|
1095
|
+
// alternative would be parsing the `DRAFT_WRITTEN` sentinel as JSON,
|
|
1096
|
+
// which is guaranteed to fail with a confusing message.
|
|
1097
|
+
emitReflectFailed("parse_error", "draft_missing", options.ref, {
|
|
1098
|
+
...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
|
|
1099
|
+
});
|
|
1100
|
+
return {
|
|
1101
|
+
failure: {
|
|
1102
|
+
schemaVersion: 2,
|
|
1103
|
+
ok: false,
|
|
1104
|
+
reason: "parse_error",
|
|
1105
|
+
error: `Agent emitted DRAFT_WRITTEN but draft file is missing or empty (${lastDraftPath}). The file-write contract failed; either the agent's file tools are broken or the path was unwritable.`,
|
|
1106
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
1107
|
+
engine: engineName,
|
|
1108
|
+
exitCode: result.exitCode,
|
|
1109
|
+
stdout: result.stdout,
|
|
1110
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
if (draftFileExists && lastDraftPath) {
|
|
1115
|
+
// Happy path: agent wrote the body to disk. Use the ref the caller
|
|
1116
|
+
// supplied (or a placeholder when omitted — the R-3 ref-mismatch guard
|
|
1117
|
+
// below has no effect when there is no expected ref).
|
|
1118
|
+
const fileContent = redactSensitiveText(fs.readFileSync(lastDraftPath, "utf8"), sensitiveValues);
|
|
1119
|
+
// Phase 6A: file-write contract carries self-rated confidence on the
|
|
1120
|
+
// `DRAFT_WRITTEN confidence=<n>` sentinel line. Extract it so the
|
|
1121
|
+
// file-write path is on equal footing with the JSON-stdout path for
|
|
1122
|
+
// auto-accept gating in `akm improve`.
|
|
1123
|
+
const draftConfidence = extractDraftConfidence(result.stdout);
|
|
1124
|
+
return {
|
|
1125
|
+
payload: {
|
|
1126
|
+
ref: options.ref ?? "",
|
|
1127
|
+
content: fileContent,
|
|
1128
|
+
...(draftConfidence !== undefined ? { confidence: draftConfidence } : {}),
|
|
1129
|
+
},
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
return { payload: parseAgentProposalPayload(result.stdout ?? "") };
|
|
1134
|
+
}
|
|
1135
|
+
catch (err) {
|
|
1136
|
+
// Reclassify cooldown/skip messages that arrive as stdout text instead of
|
|
1137
|
+
// valid proposal JSON. These are legitimate skip signals, not parse failures,
|
|
1138
|
+
// and should not pollute reflectFailedActions or recentErrors injection.
|
|
1139
|
+
const stdoutText = result.stdout ?? "";
|
|
1140
|
+
const isCooldownSignal = isStructuredCooldownSignal(stdoutText);
|
|
1141
|
+
const reason = isCooldownSignal ? "cooldown" : "parse_error";
|
|
1142
|
+
emitReflectFailed(reason, isCooldownSignal ? "stdout_cooldown_signal" : "parse_error", options.ref, {
|
|
1143
|
+
...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
|
|
1144
|
+
...(reflectLlmTelemetry(result) ?? {}),
|
|
1145
|
+
});
|
|
1146
|
+
return {
|
|
1147
|
+
failure: {
|
|
1148
|
+
schemaVersion: 2,
|
|
1149
|
+
ok: false,
|
|
1150
|
+
reason,
|
|
1151
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1152
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
1153
|
+
engine: engineName,
|
|
1154
|
+
exitCode: result.exitCode,
|
|
1155
|
+
stdout: result.stdout,
|
|
1156
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
1157
|
+
},
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Resolve the single named engine for a reflect invocation (standalone --engine
|
|
1163
|
+
* / defaults.engine, or the improve strategy's LLM-only process overlay),
|
|
1164
|
+
* throwing on any incompatible or missing engine, and validating the unattended
|
|
1165
|
+
* LLM requirement. Extracted verbatim from `akmReflect`.
|
|
1166
|
+
*/
|
|
1167
|
+
function resolveReflectRunner(options) {
|
|
1168
|
+
const config = options.config ?? loadConfig();
|
|
1169
|
+
const activeStrategy = options.improveProfile ?? config.improve?.strategies?.[config.defaults?.improveStrategy ?? "default"];
|
|
1170
|
+
let runnerSpec;
|
|
1171
|
+
if (options.runner) {
|
|
1172
|
+
runnerSpec = options.runner;
|
|
1173
|
+
}
|
|
1174
|
+
else if (Object.hasOwn(options, "runner")) {
|
|
1175
|
+
throw new ConfigError("Reflect requires an LLM engine for the active improve invocation.", "LLM_NOT_CONFIGURED", "Set defaults.llmEngine or improve.strategies.<name>.processes.reflect.engine.");
|
|
1176
|
+
}
|
|
1177
|
+
else if (options.engine) {
|
|
1178
|
+
runnerSpec = resolveEngine(options.engine, config);
|
|
1179
|
+
}
|
|
1180
|
+
else if (options.improveProfile) {
|
|
1181
|
+
const processRunner = resolveImproveProcessRunner(activeStrategy, "reflect", config);
|
|
1182
|
+
if (!processRunner) {
|
|
1183
|
+
throw new ConfigError("Reflect requires an LLM engine for the active improve strategy.", "LLM_NOT_CONFIGURED", "Set defaults.llmEngine or improve.strategies.<name>.processes.reflect.engine.");
|
|
1184
|
+
}
|
|
1185
|
+
runnerSpec = processRunner;
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
const defaultEngine = config.defaults?.engine;
|
|
1189
|
+
if (!defaultEngine) {
|
|
1190
|
+
throw new ConfigError("reflect requires --engine or defaults.engine.", "INVALID_CONFIG_FILE");
|
|
1191
|
+
}
|
|
1192
|
+
runnerSpec = resolveEngine(defaultEngine, config);
|
|
1193
|
+
}
|
|
1194
|
+
if (options.eventSource === "improve" && !runnerIsLlm(runnerSpec)) {
|
|
1195
|
+
throw new ConfigError(`Unattended improve requires an LLM engine for reflect; engine "${runnerSpec.engine ?? options.engine ?? "unknown"}" is tool-capable.`, "INVALID_CONFIG_FILE", "Set defaults.llmEngine or improve.strategies.<name>.processes.reflect.engine to an LLM engine.");
|
|
1196
|
+
}
|
|
1197
|
+
const engineName = runnerSpec.engine ?? options.engine;
|
|
1198
|
+
if (!engineName) {
|
|
1199
|
+
throw new ConfigError("Reflect requires a named engine.", "INVALID_CONFIG_FILE");
|
|
1200
|
+
}
|
|
1201
|
+
return { config, activeStrategy, runnerSpec, engineName };
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Resolve the reflect target's parsed ref + current on-disk content: enforce the
|
|
1205
|
+
* REFLECT_ALLOWED_TYPES markdown-canonical type guard (returning a terminal
|
|
1206
|
+
* `unsupported_type` failure), honour the `options.assetContent` test seam, else
|
|
1207
|
+
* best-effort load via the local file path / index lookup. Extracted verbatim
|
|
1208
|
+
* from `akmReflect`.
|
|
1209
|
+
*/
|
|
1210
|
+
async function resolveReflectSource(options, stash, emitReflectFailed) {
|
|
1211
|
+
let assetContent;
|
|
1212
|
+
let parsedRef;
|
|
1213
|
+
if (options.ref) {
|
|
1214
|
+
parsedRef = parseRefInput(options.ref);
|
|
1215
|
+
// 2a. Type guard — reflect only operates on asset types whose canonical
|
|
1216
|
+
// shape is `frontmatter + markdown body`. Refuse non-markdown types
|
|
1217
|
+
// (script / env / task) up-front so reflect never prepends YAML to a
|
|
1218
|
+
// `.ts` file or rewrites a `.env` blob as prose. See REFLECT_ALLOWED_TYPES.
|
|
1219
|
+
if (!REFLECT_ALLOWED_TYPES.has(parsedRef.type)) {
|
|
1220
|
+
// Deterministic type-guard rejection — the LLM is never invoked. Emit
|
|
1221
|
+
// with reason `unsupported_type` so the improve loop can route this to
|
|
1222
|
+
// the `reflect-skipped` action bucket instead of `reflect-failed`. See
|
|
1223
|
+
// `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1a
|
|
1224
|
+
// ("Reflect refused asset type" — ~9% of reflect-failed events).
|
|
1225
|
+
emitReflectFailed("unsupported_type", "unsupported_type", options.ref, { type: parsedRef.type });
|
|
1226
|
+
return {
|
|
1227
|
+
failure: {
|
|
1228
|
+
schemaVersion: 2,
|
|
1229
|
+
ok: false,
|
|
1230
|
+
reason: "unsupported_type",
|
|
1231
|
+
error: `Reflect refused: asset type "${parsedRef.type}" is not supported by reflect (only markdown-canonical types are allowed: ${[...REFLECT_ALLOWED_TYPES].sort().join(", ")}). Use \`akm proposal new\` or edit the file directly.`,
|
|
1232
|
+
ref: options.ref,
|
|
1233
|
+
exitCode: null,
|
|
1234
|
+
},
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
if (options.assetContent !== undefined) {
|
|
1238
|
+
// Test seam — caller pre-loaded the source content.
|
|
1239
|
+
assetContent = options.assetContent;
|
|
1240
|
+
}
|
|
1241
|
+
else {
|
|
1242
|
+
try {
|
|
1243
|
+
// Resolve the source by item_ref when planning supplied one, otherwise
|
|
1244
|
+
// use the input conceptId.
|
|
1245
|
+
const qualifiedRef = options.itemRef ?? durableImproveRef(options.ref);
|
|
1246
|
+
const localFilePath = await findAssetFilePath(qualifiedRef, stash);
|
|
1247
|
+
if (localFilePath && fs.existsSync(localFilePath)) {
|
|
1248
|
+
assetContent = fs.readFileSync(localFilePath, "utf8");
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
const entry = await lookup(parseRefInput(qualifiedRef));
|
|
1252
|
+
if (entry?.filePath && fs.existsSync(entry.filePath)) {
|
|
1253
|
+
assetContent = fs.readFileSync(entry.filePath, "utf8");
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
catch {
|
|
1258
|
+
// Index miss is non-fatal — the agent can still propose a fresh asset.
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return { assetContent, parsedRef };
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Run the agent with the optional Self-Refine loop (R-1 / #372): up to
|
|
1266
|
+
* MAX_REFINE_ITERS invocations, each injecting the prior draft as self-critique
|
|
1267
|
+
* context and exiting early on a no-op refinement. Synthesizes per-iteration
|
|
1268
|
+
* draft paths into `draftPathsToCleanup` (mutated) and returns the final agent
|
|
1269
|
+
* result + last draft path. Extracted verbatim from `akmReflect`.
|
|
1270
|
+
*/
|
|
1271
|
+
async function runReflectRefineIterations(args) {
|
|
1272
|
+
const { options, parsedRef, assetContent, feedback, schemaHints, relatedLessons, rejectedProposals, standardsContext, runnerSpec, agentEnv, draftPathsToCleanup, } = args;
|
|
1273
|
+
const MAX_REFINE_ITERS = 3;
|
|
1274
|
+
const maxRefineIters = Math.min(Math.max(1, options.maxRefineIters ?? 1), MAX_REFINE_ITERS);
|
|
892
1275
|
// Determine whether this dispatch can honour the file-write contract.
|
|
893
1276
|
// Agent CLI + OpenCode SDK runners both have filesystem access; the direct
|
|
894
|
-
// LLM HTTP runner does NOT
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1277
|
+
// LLM HTTP runner does NOT.
|
|
1278
|
+
const canRunnerWriteFile = runnerSupportsFileWrite(runnerSpec);
|
|
1279
|
+
const outputMode = runnerIsLlm(runnerSpec)
|
|
1280
|
+
? runnerSpec.connection.supportsJsonSchema
|
|
1281
|
+
? "json_schema"
|
|
1282
|
+
: "framed_markdown"
|
|
1283
|
+
: undefined;
|
|
899
1284
|
// Initialized to a sentinel; always overwritten in the first loop iteration
|
|
900
|
-
// (maxRefineIters is clamped to >= 1 above).
|
|
901
|
-
// for-loop always runs at least once, so we use a type assertion here.
|
|
1285
|
+
// (maxRefineIters is clamped to >= 1 above).
|
|
902
1286
|
let result = {};
|
|
903
1287
|
let priorDraft;
|
|
1288
|
+
let lastDraftPath;
|
|
1289
|
+
let repairAttempts = 0;
|
|
1290
|
+
for (let iter = 0; iter < maxRefineIters; iter++) {
|
|
1291
|
+
// Synthesize a fresh tmp path per iteration so refinement passes never
|
|
1292
|
+
// clobber an earlier draft (and so reading back is unambiguous).
|
|
1293
|
+
const iterDraftPath = canRunnerWriteFile ? synthesizeReflectDraftPath(options.ref) : undefined;
|
|
1294
|
+
if (iterDraftPath) {
|
|
1295
|
+
draftPathsToCleanup.push(iterDraftPath);
|
|
1296
|
+
lastDraftPath = iterDraftPath;
|
|
1297
|
+
}
|
|
1298
|
+
const { prompt, maxOutputChars } = buildReflectPrompt({
|
|
1299
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
1300
|
+
...(parsedRef?.type ? { type: parsedRef.type } : {}),
|
|
1301
|
+
...(parsedRef?.name ? { name: parsedRef.name } : {}),
|
|
1302
|
+
...(assetContent !== undefined ? { assetContent } : {}),
|
|
1303
|
+
...(feedback.length > 0 ? { feedback } : {}),
|
|
1304
|
+
...(schemaHints.length > 0 ? { schemaHints } : {}),
|
|
1305
|
+
...(relatedLessons.length > 0 ? { relatedLessons } : {}),
|
|
1306
|
+
...(options.task ? { task: options.task } : {}),
|
|
1307
|
+
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
1308
|
+
...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
|
|
1309
|
+
...(rejectedProposals.length > 0 ? { rejectedProposals } : {}),
|
|
1310
|
+
// R-1: inject prior draft as self-critique target on iterations > 0
|
|
1311
|
+
...(priorDraft !== undefined ? { priorDraft } : {}),
|
|
1312
|
+
// Issue A (#reflect-pipeline file-write contract): when the runner can
|
|
1313
|
+
// touch the filesystem, instruct the agent to write the proposal body
|
|
1314
|
+
// to a tmp file instead of inlining it in JSON. Avoids parse failures
|
|
1315
|
+
// on long bodies (e.g. knowledge/systems/KOKORO_USAGE_GUIDE 8.4KB).
|
|
1316
|
+
...(iterDraftPath ? { draftFilePath: iterDraftPath } : {}),
|
|
1317
|
+
...(outputMode ? { outputMode } : {}),
|
|
1318
|
+
});
|
|
1319
|
+
// Convert char ceiling → token cap for the LLM path: divide by 3 chars/token
|
|
1320
|
+
// (conservative — most models are 3.5–4) and add 500-char overhead for the
|
|
1321
|
+
// JSON wrapper and frontmatter block that surround the body in the response.
|
|
1322
|
+
const maxTokensForLlm = maxOutputChars !== undefined ? Math.ceil((maxOutputChars + 500) / 3) : undefined;
|
|
1323
|
+
// Every engine kind crosses the same dispatch seam. Injected spawn/timer
|
|
1324
|
+
// functions remain ordinary run options for deterministic tests.
|
|
1325
|
+
const runOptions = {
|
|
1326
|
+
stdio: "captured",
|
|
1327
|
+
parseOutput: "text",
|
|
1328
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
1329
|
+
...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
|
|
1330
|
+
...(options.runAgentOptions ?? {}),
|
|
1331
|
+
};
|
|
1332
|
+
const iterResult = await executeRunner(runnerSpec, prompt ?? "", runOptions, {
|
|
1333
|
+
llm: async (spec, _prompt, opts) =>
|
|
1334
|
+
// LLM HTTP runners cannot honor the file-write contract, so they
|
|
1335
|
+
// return structured JSON through stdout.
|
|
1336
|
+
runReflectViaLlm({
|
|
1337
|
+
prompt,
|
|
1338
|
+
connection: spec.connection,
|
|
1339
|
+
...(Object.hasOwn(opts, "timeoutMs") ? { timeoutMs: opts.timeoutMs } : {}),
|
|
1340
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
1341
|
+
priorDraft,
|
|
1342
|
+
iteration: iter,
|
|
1343
|
+
...(spec.connection.supportsJsonSchema
|
|
1344
|
+
? { responseSchema: options.ref ? REFLECT_JSON_SCHEMA : REFLECT_UNSCOPED_JSON_SCHEMA }
|
|
1345
|
+
: {}),
|
|
1346
|
+
outputMode: spec.connection.supportsJsonSchema ? "json_schema" : "framed_markdown",
|
|
1347
|
+
...(options.ref ? { targetRef: options.ref } : {}),
|
|
1348
|
+
allowRepair: repairAttempts === 0,
|
|
1349
|
+
chat: options.chat,
|
|
1350
|
+
...(maxTokensForLlm !== undefined ? { maxTokens: maxTokensForLlm } : {}),
|
|
1351
|
+
}),
|
|
1352
|
+
});
|
|
1353
|
+
const iterTelemetry = reflectLlmTelemetry(iterResult);
|
|
1354
|
+
if (iterTelemetry)
|
|
1355
|
+
repairAttempts += iterTelemetry.repairAttempts;
|
|
1356
|
+
result = iterTelemetry
|
|
1357
|
+
? {
|
|
1358
|
+
...iterResult,
|
|
1359
|
+
parsed: {
|
|
1360
|
+
...iterResult.parsed,
|
|
1361
|
+
...iterTelemetry,
|
|
1362
|
+
repairAttempts,
|
|
1363
|
+
},
|
|
1364
|
+
}
|
|
1365
|
+
: iterResult;
|
|
1366
|
+
if (!result.ok)
|
|
1367
|
+
break; // surface failure after loop
|
|
1368
|
+
// On success, extract the draft content for the next iteration.
|
|
1369
|
+
// If the agent returns the same content as the prior draft, stop early
|
|
1370
|
+
// (no-op refinement) to avoid wasting tokens on identical iterations.
|
|
1371
|
+
if (iter < maxRefineIters - 1) {
|
|
1372
|
+
const nextDraft = reflectLlmPriorDraft(result) ?? result.stdout ?? "";
|
|
1373
|
+
if (priorDraft !== undefined && nextDraft === priorDraft)
|
|
1374
|
+
break;
|
|
1375
|
+
priorDraft = nextDraft;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return { result, lastDraftPath };
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* WI-9.10: build one `akm reflect` invocation's {@link RunContext} purely
|
|
1382
|
+
* from values `akmReflect` has already resolved by the time it calls this
|
|
1383
|
+
* (stash, config, runnerSpec) plus the caller-supplied seams on `options` —
|
|
1384
|
+
* no second config load, no new db handle. reflect has no `dryRun` option
|
|
1385
|
+
* (it never writes source assets directly, only the proposal queue — see the
|
|
1386
|
+
* module docblock) so `dryRun` is always `false` here. reflect also has no
|
|
1387
|
+
* `sourceRun` option; the value below mirrors the same `reflect-${Date.now()}`
|
|
1388
|
+
* convention already used inline at proposal creation time (see
|
|
1389
|
+
* `createInput` further down this file), as a fresh, independent token —
|
|
1390
|
+
* nothing yet reads `ctx.sourceRun`.
|
|
1391
|
+
*/
|
|
1392
|
+
function buildReflectRunContext(args) {
|
|
1393
|
+
const { options, stash, config, runnerSpec } = args;
|
|
1394
|
+
return createRunContext({
|
|
1395
|
+
stashDir: stash,
|
|
1396
|
+
config,
|
|
1397
|
+
eventsCtx: options.eventsCtx ?? {},
|
|
1398
|
+
// Not yet wired into any proposal call site this stage (mirrors
|
|
1399
|
+
// buildImproveRunContext's proposalsCtx comment in improve.ts).
|
|
1400
|
+
proposalsCtx: options.ctx ?? {},
|
|
1401
|
+
chat: options.chat,
|
|
1402
|
+
getLlmConfig: () => (runnerIsLlm(runnerSpec) ? materializeLlmRunnerConnection(runnerSpec) : null),
|
|
1403
|
+
sourceRun: `reflect-${Date.now()}`,
|
|
1404
|
+
dryRun: false,
|
|
1405
|
+
signal: options.signal,
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Emit `reflect_invoked` at command entry, then build the `reflect_completed`
|
|
1410
|
+
* failure emitter every failure path in `akmReflect` uses (Fix #3 /
|
|
1411
|
+
* observability 0.8.0). Extracted verbatim (fn-size decomposition, R31) — see
|
|
1412
|
+
* the original inline comments preserved below for the "why".
|
|
1413
|
+
*
|
|
1414
|
+
* Fix #3 (observability 0.8.0): every failure path below MUST emit
|
|
1415
|
+
* `reflect_completed` so observers can close the invoke/complete loop. The
|
|
1416
|
+
* three success-side `reflect_completed` emit sites carry rich metadata
|
|
1417
|
+
* (qualityRejected, sanitized, proposalId, etc.); the failure-side emits
|
|
1418
|
+
* carry `{ok: false, reason}` plus the ref when known. Stable failure
|
|
1419
|
+
* reasons line up with `AgentFailureReason`: "parse_error", "non_zero_exit",
|
|
1420
|
+
* "cooldown", "timeout", "spawn_failed", "llm_*", plus the synthetic
|
|
1421
|
+
* "ref_mismatch" / "enoent" / "draft_missing" subtypes for cases the agent
|
|
1422
|
+
* surface conflates as "parse_error". Sub-reasons land in `subreason`.
|
|
1423
|
+
*/
|
|
1424
|
+
function emitReflectInvokedAndBuildFailureEmitter(options) {
|
|
1425
|
+
// Always emit `reflect_invoked` at command entry — observers see the
|
|
1426
|
+
// attempt regardless of downstream success/failure.
|
|
1427
|
+
appendEvent({
|
|
1428
|
+
eventType: "reflect_invoked",
|
|
1429
|
+
// Key on item_ref when planning supplied one, otherwise the conceptId.
|
|
1430
|
+
...(options.ref ? { ref: options.itemRef ?? durableImproveRef(options.ref) } : {}),
|
|
1431
|
+
metadata: {
|
|
1432
|
+
...(options.task ? { task: options.task } : {}),
|
|
1433
|
+
...(options.engine ? { engine: options.engine } : {}),
|
|
1434
|
+
// Attribution tagging: stamp the eligibility lane so reflect_invoked can be
|
|
1435
|
+
// sliced by lane downstream. See EligibilitySource.
|
|
1436
|
+
...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
|
|
1437
|
+
},
|
|
1438
|
+
}, options.eventsCtx);
|
|
1439
|
+
return (reason, subreason, ref, extra) => {
|
|
1440
|
+
appendEvent({
|
|
1441
|
+
eventType: "reflect_completed",
|
|
1442
|
+
...(ref ? { ref } : {}),
|
|
1443
|
+
metadata: {
|
|
1444
|
+
source: "reflect",
|
|
1445
|
+
ok: false,
|
|
1446
|
+
reason,
|
|
1447
|
+
subreason,
|
|
1448
|
+
...(extra ?? {}),
|
|
1449
|
+
},
|
|
1450
|
+
}, options.eventsCtx);
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
export async function akmReflect(options = {}) {
|
|
1454
|
+
const stash = resolveRunStashDir(options.stashDir);
|
|
1455
|
+
// 1. Emit reflect_invoked + build the reflect_completed failure emitter
|
|
1456
|
+
// every failure path below uses.
|
|
1457
|
+
const emitReflectFailed = emitReflectInvokedAndBuildFailureEmitter(options);
|
|
1458
|
+
// 2. Resolve target asset content (if a ref is supplied).
|
|
1459
|
+
const sourceResolved = await resolveReflectSource(options, stash, emitReflectFailed);
|
|
1460
|
+
if ("failure" in sourceResolved)
|
|
1461
|
+
return sourceResolved.failure;
|
|
1462
|
+
const { assetContent, parsedRef } = sourceResolved;
|
|
1463
|
+
// 3. Resolve exactly one named engine. Standalone reflect uses --engine or
|
|
1464
|
+
// defaults.engine; improve resolves its LLM-only strategy/process overlay.
|
|
1465
|
+
// An incompatible explicit engine is an error and never falls through.
|
|
1466
|
+
const { config, activeStrategy, runnerSpec, engineName } = resolveReflectRunner(options);
|
|
1467
|
+
// WI-9.10: RunContext, built only once config/runnerSpec exist so engine
|
|
1468
|
+
// resolution's existing error-priority ordering is undisturbed (see
|
|
1469
|
+
// buildReflectRunContext's docblock). D6: assetCtx is a fresh,
|
|
1470
|
+
// per-invocation memo — readRelatedLessons below is its genuine
|
|
1471
|
+
// content-read consumer.
|
|
1472
|
+
const ctx = buildReflectRunContext({ options, stash, config, runnerSpec });
|
|
1473
|
+
const assetCtx = ctx.withFreshAssetMemo();
|
|
1474
|
+
// 4. Build the shared prompt inputs — feedback, hints, lessons, rejected
|
|
1475
|
+
// proposals. These are stable across refinement iterations; only the
|
|
1476
|
+
// `priorDraft` field changes per-iteration (R-1 / #372).
|
|
1477
|
+
const feedback = readRecentFeedback(options.ref ? (options.itemRef ?? durableImproveRef(options.ref)) : undefined);
|
|
1478
|
+
const schemaHints = buildSchemaHints(parsedRef?.type ?? "", assetContent);
|
|
1479
|
+
const relatedLessons = options.ref && parsedRef ? await readRelatedLessons(assetCtx, stash, options.ref, parsedRef, options.itemRef) : [];
|
|
1480
|
+
// Reflexion-style verbal-RL: inject rejected proposals so the agent avoids
|
|
1481
|
+
// reproducing proposals that have already been reviewed and refused.
|
|
1482
|
+
const rejectedProposals = readRejectedProposals(stash, options.ref);
|
|
1483
|
+
// Standards "rulebook" for this target — stash convention/meta facts; empty
|
|
1484
|
+
// when none fire.
|
|
1485
|
+
const standardsContext = resolveStandardsContext(options.ref, stash);
|
|
1486
|
+
// 5. Spawn the agent — with the optional Self-Refine loop (R-1 / #372),
|
|
1487
|
+
// extracted to {@link runReflectRefineIterations}.
|
|
1488
|
+
const agentEnv = options.eventSource === "improve" ? { AKM_EVENT_SOURCE: "improve" } : {};
|
|
1489
|
+
const sensitiveValues = collectDispatchSensitiveValues(runnerSpec, {
|
|
1490
|
+
...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
|
|
1491
|
+
...(options.runAgentOptions ?? {}),
|
|
1492
|
+
});
|
|
904
1493
|
// Track every draft file path we synthesize so cleanup can remove them on
|
|
905
1494
|
// every return path (success and failure). Mirrors propose's unlink pattern
|
|
906
|
-
// in `src/commands/propose.ts
|
|
1495
|
+
// in `src/commands/proposal/propose.ts` but generalised to N refinement
|
|
907
1496
|
// iterations. Always called via {@link cleanupDrafts} below.
|
|
908
1497
|
const draftPathsToCleanup = [];
|
|
909
|
-
// Last iteration's draft path — read back if the agent wrote it.
|
|
910
|
-
let lastDraftPath;
|
|
911
1498
|
// Best-effort unlink: tolerate already-deleted files (we may have unlinked
|
|
912
1499
|
// an intermediate iteration's draft) and unwritable paths. Never throws —
|
|
913
1500
|
// the proposal result is the source of truth for the caller.
|
|
@@ -922,127 +1509,28 @@ export async function akmReflect(options = {}) {
|
|
|
922
1509
|
}
|
|
923
1510
|
}
|
|
924
1511
|
};
|
|
925
|
-
// `payload`
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
|
|
1512
|
+
// `result` / `lastDraftPath` / `payload` are populated inside the try. Hoisted
|
|
1513
|
+
// here so the post-try sections (R-3 ref guard, sanitizer, quality gate,
|
|
1514
|
+
// createProposal) can use them after the drafts have been cleaned up.
|
|
1515
|
+
let result = {};
|
|
1516
|
+
let lastDraftPath;
|
|
929
1517
|
let payload;
|
|
930
1518
|
try {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
...(relatedLessons.length > 0 ? { relatedLessons } : {}),
|
|
947
|
-
...(options.task ? { task: options.task } : {}),
|
|
948
|
-
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
949
|
-
...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
|
|
950
|
-
...(rejectedProposals.length > 0 ? { rejectedProposals } : {}),
|
|
951
|
-
// R-1: inject prior draft as self-critique target on iterations > 0
|
|
952
|
-
...(priorDraft !== undefined ? { priorDraft } : {}),
|
|
953
|
-
// Issue A (#reflect-pipeline file-write contract): when the runner can
|
|
954
|
-
// touch the filesystem, instruct the agent to write the proposal body
|
|
955
|
-
// to a tmp file instead of inlining it in JSON. Avoids parse failures
|
|
956
|
-
// on long bodies (e.g. knowledge:systems/KOKORO_USAGE_GUIDE 8.4KB).
|
|
957
|
-
...(iterDraftPath ? { draftFilePath: iterDraftPath } : {}),
|
|
958
|
-
});
|
|
959
|
-
// Convert char ceiling → token cap for the LLM path: divide by 3 chars/token
|
|
960
|
-
// (conservative — most models are 3.5–4) and add 500-char overhead for the
|
|
961
|
-
// JSON wrapper and frontmatter block that surround the body in the response.
|
|
962
|
-
const maxTokensForLlm = maxOutputChars !== undefined ? Math.ceil((maxOutputChars + 500) / 3) : undefined;
|
|
963
|
-
let iterResult;
|
|
964
|
-
if (options.runAgentOptions?.spawn) {
|
|
965
|
-
// Test seam: use raw runAgent with injected spawn so tests remain deterministic.
|
|
966
|
-
const resolvedProfile = profile;
|
|
967
|
-
if (!resolvedProfile) {
|
|
968
|
-
throw new Error("internal: reflect test-seam path requires a resolved agent profile");
|
|
969
|
-
}
|
|
970
|
-
const runOptions = {
|
|
971
|
-
stdio: "captured",
|
|
972
|
-
parseOutput: "text",
|
|
973
|
-
...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
|
|
974
|
-
...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
|
|
975
|
-
...(options.runAgentOptions ?? {}),
|
|
976
|
-
};
|
|
977
|
-
iterResult = await runAgent(resolvedProfile, prompt, runOptions);
|
|
978
|
-
}
|
|
979
|
-
else if (runnerSpec) {
|
|
980
|
-
// v2: dispatch through the unified RunnerSpec seam (X3). The `agent` /
|
|
981
|
-
// `sdk` arms route to the default profile runners; the `llm` arm is
|
|
982
|
-
// reflect-specific (wraps `runReflectViaLlm` — its bespoke iteration
|
|
983
|
-
// shape) so it is supplied as the `llm` handler.
|
|
984
|
-
const runOptions = {
|
|
985
|
-
stdio: "captured",
|
|
986
|
-
parseOutput: "text",
|
|
987
|
-
...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
|
|
988
|
-
};
|
|
989
|
-
iterResult = await executeRunner(runnerSpec, prompt ?? "", runOptions, {
|
|
990
|
-
llm: async (spec) =>
|
|
991
|
-
// LLM HTTP path — `draftFilePath` is accepted for type symmetry
|
|
992
|
-
// (see `RunReflectViaLlmOptions.draftFilePath` docstring) but is
|
|
993
|
-
// intentionally a no-op. The prompt builder above also did not
|
|
994
|
-
// include the file-write contract for this kind, so the LLM is
|
|
995
|
-
// still asked for JSON via stdout.
|
|
996
|
-
runReflectViaLlm({
|
|
997
|
-
prompt,
|
|
998
|
-
connection: spec.connection,
|
|
999
|
-
timeoutMs: spec.timeoutMs ?? (typeof resolvedTimeoutMs === "number" ? resolvedTimeoutMs : undefined),
|
|
1000
|
-
priorDraft,
|
|
1001
|
-
iteration: iter,
|
|
1002
|
-
responseSchema: REFLECT_JSON_SCHEMA,
|
|
1003
|
-
chat: options.chat,
|
|
1004
|
-
...(maxTokensForLlm !== undefined ? { maxTokens: maxTokensForLlm } : {}),
|
|
1005
|
-
}),
|
|
1006
|
-
// The `agent` arm (and only the agent arm — preserving prior behavior)
|
|
1007
|
-
// overlays `spec.timeoutMs` onto the base run options.
|
|
1008
|
-
runAgent: (profile, p, opts) => runAgent(profile, p, {
|
|
1009
|
-
...opts,
|
|
1010
|
-
...(runnerSpec.timeoutMs !== undefined ? { timeoutMs: runnerSpec.timeoutMs } : {}),
|
|
1011
|
-
}),
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
else {
|
|
1015
|
-
// Production path (v1): dispatch directly to the appropriate runner.
|
|
1016
|
-
// The fallback at the end of step 3 guarantees `profile` is set whenever
|
|
1017
|
-
// `runnerSpec` is undefined, but TS can't prove that across the loop +
|
|
1018
|
-
// await boundary — narrow into a const.
|
|
1019
|
-
const resolvedProfile = profile;
|
|
1020
|
-
if (!resolvedProfile) {
|
|
1021
|
-
throw new Error("internal: reflect v1 dispatch reached without a resolved agent profile or runnerSpec");
|
|
1022
|
-
}
|
|
1023
|
-
const runOptions = {
|
|
1024
|
-
stdio: "captured",
|
|
1025
|
-
parseOutput: "text",
|
|
1026
|
-
...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
|
|
1027
|
-
...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
|
|
1028
|
-
};
|
|
1029
|
-
iterResult = resolvedProfile.sdkMode
|
|
1030
|
-
? await runOpencodeSdk(resolvedProfile, prompt ?? "", runOptions)
|
|
1031
|
-
: await runAgent(resolvedProfile, prompt, runOptions);
|
|
1032
|
-
}
|
|
1033
|
-
result = iterResult;
|
|
1034
|
-
if (!iterResult.ok)
|
|
1035
|
-
break; // surface failure after loop
|
|
1036
|
-
// On success, extract the draft content for the next iteration.
|
|
1037
|
-
// If the agent returns the same content as the prior draft, stop early
|
|
1038
|
-
// (no-op refinement) to avoid wasting tokens on identical iterations.
|
|
1039
|
-
if (iter < maxRefineIters - 1) {
|
|
1040
|
-
const nextDraft = iterResult.stdout ?? "";
|
|
1041
|
-
if (priorDraft !== undefined && nextDraft === priorDraft)
|
|
1042
|
-
break;
|
|
1043
|
-
priorDraft = nextDraft;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1519
|
+
const iterated = await runReflectRefineIterations({
|
|
1520
|
+
options,
|
|
1521
|
+
parsedRef,
|
|
1522
|
+
assetContent,
|
|
1523
|
+
feedback,
|
|
1524
|
+
schemaHints,
|
|
1525
|
+
relatedLessons,
|
|
1526
|
+
rejectedProposals,
|
|
1527
|
+
standardsContext,
|
|
1528
|
+
runnerSpec,
|
|
1529
|
+
agentEnv,
|
|
1530
|
+
draftPathsToCleanup,
|
|
1531
|
+
});
|
|
1532
|
+
result = iterated.result;
|
|
1533
|
+
lastDraftPath = iterated.lastDraftPath;
|
|
1046
1534
|
const finalResult = result;
|
|
1047
1535
|
if (!finalResult.ok) {
|
|
1048
1536
|
// B3: ENOENT / not-found gives an actionable hint.
|
|
@@ -1051,102 +1539,30 @@ export async function akmReflect(options = {}) {
|
|
|
1051
1539
|
...(finalResult.exitCode !== undefined ? { exitCode: finalResult.exitCode } : {}),
|
|
1052
1540
|
});
|
|
1053
1541
|
return {
|
|
1054
|
-
...failureEnvelope(finalResult, options.ref),
|
|
1055
|
-
error: enoentHintMessage(profile
|
|
1542
|
+
...failureEnvelope(finalResult, options.ref, engineName),
|
|
1543
|
+
error: enoentHintMessage(runnerIsLlm(runnerSpec) ? engineName : runnerSpec.profile.bin),
|
|
1056
1544
|
};
|
|
1057
1545
|
}
|
|
1058
|
-
const envelope = failureEnvelope(finalResult, options.ref);
|
|
1059
|
-
emitReflectFailed(envelope.reason, "agent_crash", options.ref, {
|
|
1546
|
+
const envelope = failureEnvelope(finalResult, options.ref, engineName);
|
|
1547
|
+
emitReflectFailed(envelope.reason, envelope.reason === "parse_error" ? "parse_error" : "agent_crash", options.ref, {
|
|
1060
1548
|
...(envelope.exitCode !== null ? { exitCode: envelope.exitCode } : {}),
|
|
1549
|
+
...(reflectLlmTelemetry(finalResult) ?? {}),
|
|
1061
1550
|
});
|
|
1062
1551
|
return envelope;
|
|
1063
1552
|
}
|
|
1064
1553
|
// Re-alias to `result` for the downstream code that references it.
|
|
1065
1554
|
result = finalResult;
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const draftFileExists = lastDraftPath !== undefined && fs.existsSync(lastDraftPath) && fs.statSync(lastDraftPath).size > 0;
|
|
1078
|
-
const draftSignaled = stdoutSignalsDraftWritten(result.stdout);
|
|
1079
|
-
if (draftSignaled && lastDraftPath && !draftFileExists) {
|
|
1080
|
-
// Agent claimed to write the draft but the file is missing or empty.
|
|
1081
|
-
// Surface as a parse_error rather than silently falling through — the
|
|
1082
|
-
// alternative would be parsing the `DRAFT_WRITTEN` sentinel as JSON,
|
|
1083
|
-
// which is guaranteed to fail with a confusing message.
|
|
1084
|
-
emitReflectFailed("parse_error", "draft_missing", options.ref, {
|
|
1085
|
-
...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
|
|
1086
|
-
});
|
|
1087
|
-
return {
|
|
1088
|
-
schemaVersion: 1,
|
|
1089
|
-
ok: false,
|
|
1090
|
-
reason: "parse_error",
|
|
1091
|
-
error: `Agent emitted DRAFT_WRITTEN but draft file is missing or empty (${lastDraftPath}). The file-write contract failed; either the agent's file tools are broken or the path was unwritable.`,
|
|
1092
|
-
...(options.ref ? { ref: options.ref } : {}),
|
|
1093
|
-
exitCode: result.exitCode,
|
|
1094
|
-
stdout: result.stdout,
|
|
1095
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
if (draftFileExists && lastDraftPath) {
|
|
1099
|
-
// Happy path: agent wrote the body to disk. Use the ref the caller
|
|
1100
|
-
// supplied (or a placeholder when omitted — the R-3 ref-mismatch guard
|
|
1101
|
-
// below has no effect when there is no expected ref).
|
|
1102
|
-
const fileContent = fs.readFileSync(lastDraftPath, "utf8");
|
|
1103
|
-
// Phase 6A: file-write contract carries self-rated confidence on the
|
|
1104
|
-
// `DRAFT_WRITTEN confidence=<n>` sentinel line. Extract it so the
|
|
1105
|
-
// file-write path is on equal footing with the JSON-stdout path for
|
|
1106
|
-
// auto-accept gating in `akm improve`.
|
|
1107
|
-
const draftConfidence = extractDraftConfidence(result.stdout);
|
|
1108
|
-
payload = {
|
|
1109
|
-
ref: options.ref ?? "",
|
|
1110
|
-
content: fileContent,
|
|
1111
|
-
...(draftConfidence !== undefined ? { confidence: draftConfidence } : {}),
|
|
1112
|
-
};
|
|
1113
|
-
// The agent followed the file-write contract — `payload.ref` mirrors the
|
|
1114
|
-
// caller's expected ref, so the R-3 guard below cannot fire. The agent
|
|
1115
|
-
// had no opportunity to retarget the proposal. If the ref was omitted
|
|
1116
|
-
// entirely, downstream `createProposal` will reject the empty ref.
|
|
1117
|
-
}
|
|
1118
|
-
else {
|
|
1119
|
-
try {
|
|
1120
|
-
payload = parseAgentProposalPayload(result.stdout ?? "");
|
|
1121
|
-
}
|
|
1122
|
-
catch (err) {
|
|
1123
|
-
const fallback = fallbackPayloadFromRawContent(result.stdout ?? "", options.ref, profile?.sdkMode ?? false);
|
|
1124
|
-
if (fallback) {
|
|
1125
|
-
payload = fallback;
|
|
1126
|
-
}
|
|
1127
|
-
else {
|
|
1128
|
-
// Reclassify cooldown/skip messages that arrive as stdout text instead of
|
|
1129
|
-
// valid proposal JSON. These are legitimate skip signals, not parse failures,
|
|
1130
|
-
// and should not pollute reflectFailedActions or recentErrors injection.
|
|
1131
|
-
const stdoutText = result.stdout ?? "";
|
|
1132
|
-
const isCooldownSignal = isStructuredCooldownSignal(stdoutText);
|
|
1133
|
-
const reason = isCooldownSignal ? "cooldown" : "parse_error";
|
|
1134
|
-
emitReflectFailed(reason, isCooldownSignal ? "stdout_cooldown_signal" : "parse_error", options.ref, {
|
|
1135
|
-
...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
|
|
1136
|
-
});
|
|
1137
|
-
return {
|
|
1138
|
-
schemaVersion: 1,
|
|
1139
|
-
ok: false,
|
|
1140
|
-
reason,
|
|
1141
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1142
|
-
...(options.ref ? { ref: options.ref } : {}),
|
|
1143
|
-
exitCode: result.exitCode,
|
|
1144
|
-
stdout: result.stdout,
|
|
1145
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1555
|
+
const resolved = resolveReflectPayload({
|
|
1556
|
+
result,
|
|
1557
|
+
lastDraftPath,
|
|
1558
|
+
sensitiveValues,
|
|
1559
|
+
options,
|
|
1560
|
+
engineName,
|
|
1561
|
+
emitReflectFailed,
|
|
1562
|
+
});
|
|
1563
|
+
if ("failure" in resolved)
|
|
1564
|
+
return resolved.failure;
|
|
1565
|
+
payload = resolved.payload;
|
|
1150
1566
|
}
|
|
1151
1567
|
finally {
|
|
1152
1568
|
// Always remove tmp draft files — success, failure, or exception. Returns
|
|
@@ -1155,28 +1571,31 @@ export async function akmReflect(options = {}) {
|
|
|
1155
1571
|
// draft paths.
|
|
1156
1572
|
cleanupDrafts();
|
|
1157
1573
|
}
|
|
1574
|
+
payload = { ...payload, content: redactSensitiveText(payload.content, sensitiveValues) };
|
|
1158
1575
|
// 6b. Validate payload.ref === options.ref (R-3 / #366).
|
|
1159
1576
|
// A hallucinating agent can silently retarget proposals to a different ref.
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1577
|
+
// Parse both current refs so an optional bundle prefix does not cause a false
|
|
1578
|
+
// positive, then reject genuine concept mismatches.
|
|
1162
1579
|
// References: CRITIC (arXiv:2305.11738), CoVe (arXiv:2309.11495).
|
|
1163
1580
|
if (options.ref) {
|
|
1164
1581
|
try {
|
|
1165
|
-
const expectedParsed =
|
|
1166
|
-
const actualParsed =
|
|
1582
|
+
const expectedParsed = parseRefInput(options.ref);
|
|
1583
|
+
const actualParsed = parseRefInput(payload.ref);
|
|
1167
1584
|
// Compare type + name (drop origin — agent may omit origin prefix).
|
|
1168
1585
|
if (expectedParsed.type !== actualParsed.type || expectedParsed.name !== actualParsed.name) {
|
|
1169
1586
|
emitReflectFailed("parse_error", "ref_mismatch", options.ref, {
|
|
1170
1587
|
expectedRef: options.ref,
|
|
1171
1588
|
actualRef: payload.ref,
|
|
1172
1589
|
...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
|
|
1590
|
+
...(reflectLlmTelemetry(result) ?? {}),
|
|
1173
1591
|
});
|
|
1174
1592
|
return {
|
|
1175
|
-
schemaVersion:
|
|
1593
|
+
schemaVersion: 2,
|
|
1176
1594
|
ok: false,
|
|
1177
1595
|
reason: "parse_error",
|
|
1178
1596
|
error: `Agent retargeted proposal: expected ref "${options.ref}" but got "${payload.ref}". Proposal rejected to prevent silent ref hallucination.`,
|
|
1179
1597
|
ref: options.ref,
|
|
1598
|
+
engine: engineName,
|
|
1180
1599
|
exitCode: result.exitCode,
|
|
1181
1600
|
stdout: result.stdout,
|
|
1182
1601
|
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
@@ -1184,251 +1603,21 @@ export async function akmReflect(options = {}) {
|
|
|
1184
1603
|
}
|
|
1185
1604
|
}
|
|
1186
1605
|
catch {
|
|
1187
|
-
//
|
|
1606
|
+
// parseRefInput failure means the agent returned a malformed ref — already
|
|
1188
1607
|
// caught downstream by createProposal; allow it to surface naturally.
|
|
1189
1608
|
}
|
|
1190
1609
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
return loadConfig();
|
|
1204
|
-
}
|
|
1205
|
-
catch {
|
|
1206
|
-
return undefined;
|
|
1207
|
-
}
|
|
1208
|
-
})();
|
|
1209
|
-
const chatFn = options.chat ?? chatCompletion;
|
|
1210
|
-
const qualityGateEnabled = isLlmFeatureEnabled(runtimeConfig, "proposal_quality_gate") ||
|
|
1211
|
-
isLlmFeatureEnabled(runtimeConfig, "lesson_quality_gate");
|
|
1212
|
-
if (qualityGateEnabled && runtimeConfig) {
|
|
1213
|
-
const assetContent = (() => {
|
|
1214
|
-
if (!options.ref)
|
|
1215
|
-
return null;
|
|
1216
|
-
try {
|
|
1217
|
-
const refParsed = parseAssetRef(options.ref);
|
|
1218
|
-
const candidates = [
|
|
1219
|
-
path.join(stash, `${refParsed.type}s`, `${refParsed.name}.md`),
|
|
1220
|
-
path.join(stash, `${refParsed.type}s`, refParsed.name, "index.md"),
|
|
1221
|
-
];
|
|
1222
|
-
for (const p of candidates) {
|
|
1223
|
-
if (fs.existsSync(p))
|
|
1224
|
-
return fs.readFileSync(p, "utf8");
|
|
1225
|
-
}
|
|
1226
|
-
return null;
|
|
1227
|
-
}
|
|
1228
|
-
catch {
|
|
1229
|
-
return null;
|
|
1230
|
-
}
|
|
1231
|
-
})();
|
|
1232
|
-
const judgeResult = await runLessonQualityJudge(runtimeConfig, payload.content, assetContent ?? "", chatFn);
|
|
1233
|
-
if (!judgeResult.pass) {
|
|
1234
|
-
// Quality gate rejected the proposal — surface as parse_error so the
|
|
1235
|
-
// improve orchestrator can log it and move on without crashing.
|
|
1236
|
-
appendEvent({
|
|
1237
|
-
eventType: "reflect_completed",
|
|
1238
|
-
ref: payload.ref,
|
|
1239
|
-
metadata: {
|
|
1240
|
-
source: "reflect",
|
|
1241
|
-
qualityRejected: true,
|
|
1242
|
-
qualityScore: judgeResult.score,
|
|
1243
|
-
qualityReason: judgeResult.reason,
|
|
1244
|
-
},
|
|
1245
|
-
});
|
|
1246
|
-
return {
|
|
1247
|
-
schemaVersion: 1,
|
|
1248
|
-
ok: false,
|
|
1249
|
-
reason: "parse_error",
|
|
1250
|
-
error: `Reflect proposal quality gate rejected: score=${judgeResult.score}, reason="${judgeResult.reason}"`,
|
|
1251
|
-
...(options.ref ? { ref: options.ref } : {}),
|
|
1252
|
-
exitCode: result.exitCode,
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
// 7b. Reflect content-preservation rails:
|
|
1257
|
-
// - Restore source frontmatter so reflect can never strip indexable
|
|
1258
|
-
// fields (`description`, `when_to_use`, `tags`, ...).
|
|
1259
|
-
// - Reset protected identity fields (`name`, `ref`, `id`, `slug`,
|
|
1260
|
-
// `type`) the LLM tried to change.
|
|
1261
|
-
// - Reject proposals that shrink/expand the body past safe ratios.
|
|
1262
|
-
//
|
|
1263
|
-
// See REFLECT_ALLOWED_TYPES / sanitizeReflectPayload for the underlying
|
|
1264
|
-
// hypotheses + observed regressions (`8737ab63`, `26941510`, and the
|
|
1265
|
-
// catastrophic-shrinkage cases from the May 2026 review).
|
|
1266
|
-
const sanitizeOutcome = sanitizeReflectPayload({ content: payload.content, ...(payload.frontmatter ? { frontmatter: payload.frontmatter } : {}) }, assetContent, payload.ref);
|
|
1267
|
-
if (sanitizeOutcome.reject) {
|
|
1268
|
-
appendEvent({
|
|
1269
|
-
eventType: "reflect_completed",
|
|
1270
|
-
ref: payload.ref,
|
|
1271
|
-
metadata: {
|
|
1272
|
-
source: "reflect",
|
|
1273
|
-
sanitized: true,
|
|
1274
|
-
rejected: true,
|
|
1275
|
-
rejectReason: sanitizeOutcome.reject.error,
|
|
1276
|
-
...(sanitizeOutcome.warnings.length > 0 ? { sanitizerWarnings: sanitizeOutcome.warnings } : {}),
|
|
1277
|
-
},
|
|
1278
|
-
});
|
|
1279
|
-
return {
|
|
1280
|
-
schemaVersion: 1,
|
|
1281
|
-
ok: false,
|
|
1282
|
-
reason: sanitizeOutcome.reject.reason,
|
|
1283
|
-
error: sanitizeOutcome.reject.error,
|
|
1284
|
-
...(options.ref ? { ref: options.ref } : {}),
|
|
1285
|
-
exitCode: result.exitCode,
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
payload = {
|
|
1289
|
-
...payload,
|
|
1290
|
-
content: sanitizeOutcome.content,
|
|
1291
|
-
...(sanitizeOutcome.frontmatter ? { frontmatter: sanitizeOutcome.frontmatter } : {}),
|
|
1292
|
-
};
|
|
1293
|
-
// 7c. Noise gate (#580): never queue a proposal whose sanitized content is
|
|
1294
|
-
// identical to the current asset (empty diff) or differs only cosmetically
|
|
1295
|
-
// (whitespace reflow, code-fence language hints, YAML scalar re-folding).
|
|
1296
|
-
// Pure deterministic text comparison — see `reflect-noise.ts`. Runs before
|
|
1297
|
-
// the draftMode branch so self-consistency sampling never votes a no-op
|
|
1298
|
-
// candidate into the queue either. Skipped when there is no source asset
|
|
1299
|
-
// (new-asset proposals have nothing to diff against).
|
|
1300
|
-
if (assetContent !== undefined) {
|
|
1301
|
-
const changeKind = classifyReflectChange(assetContent, payload.content);
|
|
1302
|
-
// 'low-value' is config-gated (#639). DEFAULT OFF — absent = byte-identical
|
|
1303
|
-
// pre-#639 behaviour (low-value treated the same as substantive). Resolved
|
|
1304
|
-
// by the caller from the ACTIVE improve profile's
|
|
1305
|
-
// `processes.reflect.lowValueFilter.enabled` and passed via options, so the
|
|
1306
|
-
// running profile (not a hardcoded `profiles.improve.default`) decides.
|
|
1307
|
-
const lowValueFilterEnabled = options.lowValueFilter === true;
|
|
1308
|
-
const isDeferred = changeKind === "noop" || changeKind === "cosmetic" || (changeKind === "low-value" && lowValueFilterEnabled);
|
|
1309
|
-
if (isDeferred) {
|
|
1310
|
-
const subreason = changeKind === "noop"
|
|
1311
|
-
? "reflect_skipped_noop"
|
|
1312
|
-
: changeKind === "low-value"
|
|
1313
|
-
? "reflect_skipped_low_value"
|
|
1314
|
-
: "reflect_skipped_cosmetic";
|
|
1315
|
-
emitReflectFailed("no_change", subreason, options.ref, { changeKind });
|
|
1316
|
-
return {
|
|
1317
|
-
schemaVersion: 1,
|
|
1318
|
-
ok: false,
|
|
1319
|
-
reason: "no_change",
|
|
1320
|
-
error: changeKind === "noop"
|
|
1321
|
-
? `Reflect skipped: proposed content for ${payload.ref} is identical to the current asset (empty diff); no proposal created.`
|
|
1322
|
-
: changeKind === "low-value"
|
|
1323
|
-
? `Reflect skipped: proposed content for ${payload.ref} is a low-value prose micro-rewrite (few changed tokens, no structural changes); no proposal created.`
|
|
1324
|
-
: `Reflect skipped: proposed content for ${payload.ref} is a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes); no proposal created.`,
|
|
1325
|
-
...(options.ref ? { ref: options.ref } : {}),
|
|
1326
|
-
exitCode: result.exitCode,
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
// 8. Create the proposal. The proposal queue is the ONLY thing reflect
|
|
1331
|
-
// writes — promotion to a real asset is gated by `akm proposal accept`.
|
|
1332
|
-
//
|
|
1333
|
-
// R-4 / #373: Stamp `derived_from_reflect: true` in the frontmatter of any
|
|
1334
|
-
// lesson proposal generated by reflect. This provenance marker lets
|
|
1335
|
-
// `readRelatedLessons` exclude echo-chamber lessons (lessons that originate
|
|
1336
|
-
// from prior reflect runs on the same skill) unless independent feedback
|
|
1337
|
-
// evidence exists. ExpeL arXiv:2308.10144 — reject rules without success/
|
|
1338
|
-
// failure differential from independent evidence.
|
|
1339
|
-
const isLessonProposal = (() => {
|
|
1340
|
-
try {
|
|
1341
|
-
return parseAssetRef(payload.ref).type === "lesson";
|
|
1342
|
-
}
|
|
1343
|
-
catch {
|
|
1344
|
-
return false;
|
|
1345
|
-
}
|
|
1346
|
-
})();
|
|
1347
|
-
const basePayloadFrontmatter = payload.frontmatter ?? {};
|
|
1348
|
-
const payloadFrontmatterWithProvenance = isLessonProposal
|
|
1349
|
-
? { ...basePayloadFrontmatter, derived_from_reflect: true }
|
|
1350
|
-
: basePayloadFrontmatter;
|
|
1351
|
-
// Draft mode: skip DB persistence — the SC sampling loop in improve.ts persists
|
|
1352
|
-
// only the majority-vote winner (R-2 / #389). Return a synthetic proposal so
|
|
1353
|
-
// pickMajorityVote can compare content via Jaccard similarity.
|
|
1354
|
-
if (options.draftMode) {
|
|
1355
|
-
const draftProposal = {
|
|
1356
|
-
id: `sc-draft-${Date.now()}`,
|
|
1357
|
-
ref: payload.ref,
|
|
1358
|
-
source: "reflect",
|
|
1359
|
-
status: "pending",
|
|
1360
|
-
createdAt: new Date().toISOString(),
|
|
1361
|
-
updatedAt: new Date().toISOString(),
|
|
1362
|
-
payload: {
|
|
1363
|
-
content: payload.content,
|
|
1364
|
-
...(Object.keys(payloadFrontmatterWithProvenance).length > 0
|
|
1365
|
-
? { frontmatter: payloadFrontmatterWithProvenance }
|
|
1366
|
-
: {}),
|
|
1367
|
-
},
|
|
1368
|
-
// Phase 6A: preserve confidence on the synthetic draft so the SC majority
|
|
1369
|
-
// winner carries the score through to the persisted proposal.
|
|
1370
|
-
...(typeof payload.confidence === "number" ? { confidence: payload.confidence } : {}),
|
|
1371
|
-
};
|
|
1372
|
-
return {
|
|
1373
|
-
schemaVersion: 1,
|
|
1374
|
-
ok: true,
|
|
1375
|
-
proposal: draftProposal,
|
|
1376
|
-
ref: draftProposal.ref,
|
|
1377
|
-
agentProfile: resolvedProfileName,
|
|
1378
|
-
durationMs: result.durationMs,
|
|
1379
|
-
};
|
|
1380
|
-
}
|
|
1381
|
-
const createInput = {
|
|
1382
|
-
ref: payload.ref,
|
|
1383
|
-
source: "reflect",
|
|
1384
|
-
sourceRun: `reflect-${Date.now()}`,
|
|
1385
|
-
payload: {
|
|
1386
|
-
content: payload.content,
|
|
1387
|
-
...(Object.keys(payloadFrontmatterWithProvenance).length > 0
|
|
1388
|
-
? { frontmatter: payloadFrontmatterWithProvenance }
|
|
1389
|
-
: {}),
|
|
1390
|
-
},
|
|
1391
|
-
// Phase 6A: forward LLM-reported confidence into the proposal record.
|
|
1392
|
-
// `parseAgentProposalPayload` already clamps to [0, 1] and drops non-
|
|
1393
|
-
// finite values; `createProposal` runs its own sanitizer as a safety net.
|
|
1394
|
-
...(typeof payload.confidence === "number" ? { confidence: payload.confidence } : {}),
|
|
1395
|
-
// Attribution tagging: persist the eligibility lane on the proposal so it
|
|
1396
|
-
// survives to accept/reject/revert time even across runs. See EligibilitySource.
|
|
1397
|
-
...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
|
|
1398
|
-
};
|
|
1399
|
-
const proposalResult = createProposal(stash, createInput, options.ctx);
|
|
1400
|
-
if (isProposalSkipped(proposalResult)) {
|
|
1401
|
-
// Dedup/cooldown guard fired — surface as a "cooldown" reason (not "parse_error")
|
|
1402
|
-
// so the improve orchestrator can distinguish legitimate skips from real failures
|
|
1403
|
-
// and exclude them from recentErrors/avoidPatterns injection.
|
|
1404
|
-
emitReflectFailed("cooldown", "proposal_skipped", options.ref, {
|
|
1405
|
-
proposalSkipReason: proposalResult.reason,
|
|
1406
|
-
});
|
|
1407
|
-
return {
|
|
1408
|
-
schemaVersion: 1,
|
|
1409
|
-
ok: false,
|
|
1410
|
-
reason: "cooldown",
|
|
1411
|
-
error: `Proposal skipped (${proposalResult.reason}): ${proposalResult.message}`,
|
|
1412
|
-
...(options.ref ? { ref: options.ref } : {}),
|
|
1413
|
-
exitCode: null,
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
const proposal = proposalResult;
|
|
1417
|
-
appendEvent({
|
|
1418
|
-
eventType: "reflect_completed",
|
|
1419
|
-
ref: proposal.ref,
|
|
1420
|
-
metadata: {
|
|
1421
|
-
proposalId: proposal.id,
|
|
1422
|
-
source: "reflect",
|
|
1423
|
-
agentProfile: resolvedProfileName,
|
|
1424
|
-
},
|
|
1610
|
+
return finalizeReflectProposal({
|
|
1611
|
+
payload,
|
|
1612
|
+
assetContent,
|
|
1613
|
+
result,
|
|
1614
|
+
options,
|
|
1615
|
+
engineName,
|
|
1616
|
+
config,
|
|
1617
|
+
activeStrategy,
|
|
1618
|
+
runnerSpec,
|
|
1619
|
+
feedback,
|
|
1620
|
+
stash,
|
|
1621
|
+
emitReflectFailed,
|
|
1425
1622
|
});
|
|
1426
|
-
return {
|
|
1427
|
-
schemaVersion: 1,
|
|
1428
|
-
ok: true,
|
|
1429
|
-
proposal,
|
|
1430
|
-
ref: proposal.ref,
|
|
1431
|
-
agentProfile: resolvedProfileName,
|
|
1432
|
-
durationMs: result.durationMs,
|
|
1433
|
-
};
|
|
1434
1623
|
}
|