akm-cli 0.9.16 → 0.9.17-alpha.10
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 +2101 -0
- package/STABILITY.md +11 -10
- package/dist/akm +124 -193
- package/dist/akm-migrate +38 -19
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/prompts/consolidate-system.md +4 -11
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +6 -2
- package/dist/cli/unknown-flags.js +24 -1
- package/dist/cli.js +68 -10
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +6 -0
- package/dist/commands/health/archive-usage.js +92 -0
- package/dist/commands/health/checks.js +83 -74
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +1 -42
- package/dist/commands/health/improve-metrics.js +136 -591
- package/dist/commands/health/md-report.js +1 -6
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +14 -120
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -74
- package/dist/commands/health.js +145 -143
- package/dist/commands/improve/consolidate/chunking.js +26 -117
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +589 -1127
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1041
- package/dist/commands/improve/eligibility.js +126 -390
- package/dist/commands/improve/execution.js +8 -10
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +75 -169
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +52 -4
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +480 -1074
- package/dist/commands/improve/ledger.js +119 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +415 -1073
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +16 -118
- package/dist/commands/improve/memory/memory-improve.js +266 -14
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -15
- package/dist/commands/improve/preparation.js +779 -2319
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +642 -1353
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +92 -0
- package/dist/commands/improve/salience.js +41 -240
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +322 -0
- package/dist/commands/lint/base-linter.js +37 -15
- package/dist/commands/proposal/drain.js +261 -578
- package/dist/commands/proposal/proposal-cli.js +19 -20
- package/dist/commands/proposal/proposal-types.js +31 -24
- package/dist/commands/proposal/proposal.js +38 -8
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +1097 -1394
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +22 -89
- package/dist/commands/read/curate.js +105 -462
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +57 -108
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +4 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +127 -29
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/source-manage.js +9 -2
- package/dist/commands/sources/stash-cli.js +24 -4
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +12 -13
- package/dist/commands/tasks/tasks.js +350 -936
- package/dist/commands/tasks/validate.js +26 -24
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +2 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +42 -12
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/asset-placement.js +4 -13
- package/dist/core/asset/frontmatter.js +106 -1
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +285 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +9 -34
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +184 -111
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +54 -125
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/index-config.js +9 -48
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-change.js +13 -5
- package/dist/core/file-lock.js +76 -173
- package/dist/core/improve-result.js +35 -7
- package/dist/core/improve-types.js +0 -1
- package/dist/core/logs-db.js +2 -2
- package/dist/core/loopback.js +7 -12
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/parse.js +13 -16
- package/dist/core/paths.js +0 -24
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +123 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/time.js +0 -20
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +294 -1005
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +77 -73
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +141 -154
- package/dist/indexer/indexer.js +400 -1124
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +96 -90
- package/dist/indexer/passes/metadata.js +132 -219
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +2 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +190 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +36 -7
- package/dist/indexer/walk/walker.js +3 -4
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/client.js +18 -6
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +11 -17
- package/dist/llm/feature-gate.js +0 -8
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +25 -38
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +69 -31
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +48 -3
- package/dist/output/text/show-format.js +13 -17
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +42948 -92369
- package/dist/scripts/akm-migrate.js +42935 -92354
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +83 -21
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +209 -0
- package/dist/storage/repositories/index-connection.js +39 -72
- package/dist/storage/repositories/index-entries-repository.js +131 -129
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +101 -268
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +257 -325
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +104 -1
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +130 -0
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +191 -302
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +141 -748
- package/dist/tasks/backends/schtasks.js +119 -623
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +20 -238
- package/dist/tasks/scheduler-invocation.js +136 -244
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +368 -679
- package/dist/tasks/source/parse-task-source.js +55 -9
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +464 -88
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +43 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
- package/docs/reference/cli.md +232 -135
- package/docs/reference/configuration.md +71 -57
- package/docs/reference/data-and-telemetry.md +20 -21
- package/docs/reference/tasks.md +105 -39
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +115 -738
- package/schemas/akm-workflow.json +1 -0
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -149
- package/dist/commands/improve/distill/promote-memory.js +0 -291
- package/dist/commands/improve/distill/quality-gate.js +0 -337
- package/dist/commands/improve/eval-cases.js +0 -52
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -31
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/db/graph-db.js +0 -444
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/graph/graph-extraction.js +0 -1108
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/search/ranking-types.js +0 -4
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/llm/graph-extract.js +0 -728
- package/dist/llm/metadata-enhance.js +0 -96
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/tasks/source/task-to-v3.js +0 -507
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -77
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -2,39 +2,20 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* never re-adjudicates a current run's output (decision #2).
|
|
19
|
-
* - Hard guardrails enforced in code: a `maxAccepts` ceiling checked *before*
|
|
20
|
-
* the promote loop (remainder → `skippedByCap`); `maxDiffLines` defers large
|
|
21
|
-
* accepts; `applyMode: "queue"` (the safe default) never promotes (stage
|
|
22
|
-
* only); `rejectEmpty` rejects empty / near-empty diffs.
|
|
23
|
-
* - The judgment tier (Phase 3) adjudicates the deferred items: when a
|
|
24
|
-
* `judgment` RunnerSpec is supplied the engine pre-fetches context (the live
|
|
25
|
-
* asset + sibling pending proposals for the same ref) into a prompt,
|
|
26
|
-
* dispatches it through the shared resolved/lowered execution boundary,
|
|
27
|
-
* and performs the resulting accept / reject *itself* (the runner only
|
|
28
|
-
* judges).
|
|
29
|
-
* Items the runner cannot resolve — and any deferred items when no runner is
|
|
30
|
-
* configured — surface a `triage_deferred` event so "enabled, no agent"
|
|
31
|
-
* never silently looks like full success.
|
|
32
|
-
*
|
|
33
|
-
* The promote / reject functions and the runner dispatch are injectable
|
|
34
|
-
* (mirrors reflect's dual test seams) so tests can run the full engine without
|
|
35
|
-
* touching the filesystem or spawning a process.
|
|
5
|
+
* The proposal drain behind `akm proposal drain` and improve's triage pre-pass.
|
|
6
|
+
* One rule decides the pending backlog:
|
|
7
|
+
* - an empty diff is rejected;
|
|
8
|
+
* - a proposal whose quality judge passed on this exact content (a `staged`
|
|
9
|
+
* gate decision carrying its content hash) is accepted, unless its target
|
|
10
|
+
* changed since mint — then it is auto-rejected as `stale-target`, never
|
|
11
|
+
* overwritten;
|
|
12
|
+
* - everything else needs a judge: the judgment tier decides it when a runner
|
|
13
|
+
* is configured, and whatever stays undecided is left for review
|
|
14
|
+
* (`review_needed` in the improve ledger).
|
|
15
|
+
* `maxAccepts` caps promotions across both tiers; `applyMode: "queue"` never
|
|
16
|
+
* promotes; `excludeIds` keeps this run's fresh proposals out; a proposal the
|
|
17
|
+
* distill quality gate routed to a human is left for that human.
|
|
36
18
|
*/
|
|
37
|
-
import { createHash } from "node:crypto";
|
|
38
19
|
import fs from "node:fs";
|
|
39
20
|
import path from "node:path";
|
|
40
21
|
import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
@@ -44,165 +25,111 @@ import { ConfigError } from "../../core/errors.js";
|
|
|
44
25
|
import { appendEvent } from "../../core/events.js";
|
|
45
26
|
import { escapeJsonStringControls, stripCodeFences, stripThinkBlocks } from "../../core/parse.js";
|
|
46
27
|
import { info, warn } from "../../core/warn.js";
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
28
|
+
import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js";
|
|
29
|
+
import { assertRunnerCredentials, runExecution, } from "../../integrations/agent/runner-dispatch.js";
|
|
30
|
+
import { errMessage, noticeSet } from "../improve/stage.js";
|
|
49
31
|
import { akmProposalAccept, akmProposalReject } from "./proposal.js";
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
export function contentBodyLineCount(content) {
|
|
56
|
-
// Reuse the canonical frontmatter parser so CRLF / BOM are handled
|
|
57
|
-
// consistently with the rest of the stash (parseFrontmatter returns the body
|
|
58
|
-
// in `content`).
|
|
59
|
-
return parseFrontmatter(content)
|
|
60
|
-
.content.split("\n")
|
|
61
|
-
.filter((line) => line.trim().length > 0).length;
|
|
62
|
-
}
|
|
63
|
-
/** Total line count of the proposed content (matches the bulk-accept measure). */
|
|
64
|
-
export function contentLineCount(content) {
|
|
65
|
-
return content.split("\n").length;
|
|
66
|
-
}
|
|
67
|
-
/** An empty / near-empty diff has no meaningful body content. */
|
|
32
|
+
import { isRetireProposal, STALE_TARGET_GATE_REASON } from "./proposal-types.js";
|
|
33
|
+
import { listProposals, listProposalsReadOnly, preflightProposalPromotion, proposalContent, proposalContentHash, readFreshProposalTarget, recordGateDecision, } from "./repository.js";
|
|
34
|
+
/** The gate label on every decision the drain records. */
|
|
35
|
+
const DRAIN_GATE = "triage";
|
|
36
|
+
/** An empty diff: no non-blank body line outside the frontmatter. */
|
|
68
37
|
export function isEmptyDiff(proposal) {
|
|
69
38
|
const content = proposalContent(proposal);
|
|
70
39
|
if (content.trim().length === 0)
|
|
71
40
|
return true;
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* Decide a deterministic verdict for a single backlog proposal under `policy`.
|
|
76
|
-
* Returns `null` when no rule applies (the proposal is left pending untouched).
|
|
77
|
-
*/
|
|
78
|
-
export function classifyProposal(proposal, policy, maxDiffLines) {
|
|
79
|
-
const content = proposalContent(proposal);
|
|
80
|
-
// Empty / near-empty diffs reject first (the reject-empty floor).
|
|
81
|
-
if (policy.rejectEmpty && isEmptyDiff(proposal)) {
|
|
82
|
-
return { verdict: "reject", reason: "empty diff", gate: { reason: "empty-diff" } };
|
|
83
|
-
}
|
|
84
|
-
const rule = policy.accept.find((r) => {
|
|
85
|
-
if (r.generator !== proposal.source)
|
|
86
|
-
return false;
|
|
87
|
-
if (r.requireType !== undefined) {
|
|
88
|
-
const fm = parseFrontmatter(proposalContent(proposal)).data;
|
|
89
|
-
if (typeof fm.type !== "string" || fm.type !== r.requireType)
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
return true;
|
|
93
|
-
});
|
|
94
|
-
if (rule) {
|
|
95
|
-
const lines = contentLineCount(content);
|
|
96
|
-
const body = contentBodyLineCount(content);
|
|
97
|
-
// Per-rule and global diff bounds defer large accepts (no silent rewrites).
|
|
98
|
-
const effectiveMax = Math.min(rule.maxDiffLines ?? Number.POSITIVE_INFINITY, maxDiffLines ?? Number.POSITIVE_INFINITY);
|
|
99
|
-
if (lines > effectiveMax) {
|
|
100
|
-
return {
|
|
101
|
-
verdict: "defer",
|
|
102
|
-
reason: "mid-band",
|
|
103
|
-
gate: { reason: "max-diff-lines", measured: lines, thresholds: { maxDiffLines: effectiveMax } },
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
if (rule.minContentLines !== undefined && body < rule.minContentLines) {
|
|
107
|
-
// Too little content to confidently auto-accept — leave for judgment.
|
|
108
|
-
return {
|
|
109
|
-
verdict: "defer",
|
|
110
|
-
reason: "mid-band",
|
|
111
|
-
gate: { reason: "min-content-lines", measured: body, thresholds: { minContentLines: rule.minContentLines } },
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return { verdict: "accept", gate: { reason: "policy-accept" } };
|
|
115
|
-
}
|
|
116
|
-
if (policy.defer.includes(proposal.source)) {
|
|
117
|
-
const reason = deferReasonForSource(proposal.source);
|
|
118
|
-
return { verdict: "defer", reason, gate: { reason } };
|
|
119
|
-
}
|
|
120
|
-
// No matching rule — leave pending, untouched.
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
function deferReasonForSource(source) {
|
|
124
|
-
return source === "distill" ? "possible-dup" : "mid-band";
|
|
41
|
+
return !parseFrontmatter(content)
|
|
42
|
+
.content.split("\n")
|
|
43
|
+
.some((line) => line.trim().length > 0);
|
|
125
44
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Map a thrown error's message to one of `DrainResult.failed`'s stable reason
|
|
128
|
-
* codes, falling back to `fallback` for anything not specifically recognized.
|
|
129
|
-
* Recognizes the write-time guards a proposal can trip during promotion
|
|
130
|
-
* (see repository.ts's `promoteProposalWithLease` / `preflightProposalPromotion`).
|
|
131
|
-
*/
|
|
132
45
|
function categorizeDrainFailure(message, fallback) {
|
|
133
46
|
if (/target (?:changed after|was created after) proposal/.test(message))
|
|
134
|
-
return
|
|
47
|
+
return STALE_TARGET_GATE_REASON;
|
|
135
48
|
if (/failed validation:/.test(message))
|
|
136
49
|
return "validation";
|
|
137
50
|
return fallback;
|
|
138
51
|
}
|
|
139
|
-
function pushDrainFailure(result, id, err, fallbackReason) {
|
|
140
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
-
result.failed.push({ id, reason: categorizeDrainFailure(message, fallbackReason), detail: message });
|
|
142
|
-
return message;
|
|
143
|
-
}
|
|
144
52
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
53
|
+
* The one accept path both tiers share. A dry run exercises the same stamped
|
|
54
|
+
* candidate, lint and freshness boundary a promotion would (tests that pass no
|
|
55
|
+
* config keep the classification-only seam). A stale target is not a merit
|
|
56
|
+
* rejection, so instead of failing identically every run it is auto-rejected
|
|
57
|
+
* once; the ledger records `failed`, keeping the ref re-proposable.
|
|
149
58
|
*/
|
|
150
|
-
function
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
59
|
+
async function acceptProposal(opts, proposal, id, reason, promoteFn, rejectFn) {
|
|
60
|
+
const gateDecision = { outcome: "auto-accepted", reason, gate: DRAIN_GATE };
|
|
61
|
+
try {
|
|
62
|
+
if (!opts.dryRun) {
|
|
63
|
+
await promoteFn({
|
|
64
|
+
stashDir: opts.stashDir,
|
|
65
|
+
id,
|
|
66
|
+
...(opts.target ? { target: opts.target } : {}),
|
|
67
|
+
...(opts.config ? { config: opts.config } : {}),
|
|
68
|
+
gateDecision,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else if (opts.config) {
|
|
72
|
+
if (!proposal)
|
|
73
|
+
throw new Error(`Proposal ${id} disappeared during drain preflight.`);
|
|
74
|
+
const preflight = preflightProposalPromotion(opts.config, proposal, {
|
|
75
|
+
...(opts.target ? { target: opts.target } : {}),
|
|
76
|
+
gateDecision,
|
|
77
|
+
});
|
|
78
|
+
readFreshProposalTarget(proposal, preflight.assetPath, preflight.stampedContent);
|
|
79
|
+
}
|
|
80
|
+
return "promoted";
|
|
155
81
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
82
|
+
catch (err) {
|
|
83
|
+
const message = errMessage(err);
|
|
84
|
+
if (categorizeDrainFailure(message, "") !== STALE_TARGET_GATE_REASON)
|
|
85
|
+
return { message };
|
|
86
|
+
if (opts.dryRun)
|
|
87
|
+
return "rejected";
|
|
88
|
+
try {
|
|
89
|
+
await rejectFn({
|
|
90
|
+
stashDir: opts.stashDir,
|
|
91
|
+
id,
|
|
92
|
+
reason: `stale-target: ${message}`,
|
|
93
|
+
gateDecision: { outcome: "auto-rejected", reason: STALE_TARGET_GATE_REASON, gate: DRAIN_GATE },
|
|
94
|
+
});
|
|
95
|
+
return "rejected";
|
|
96
|
+
}
|
|
97
|
+
catch (rejectErr) {
|
|
98
|
+
warn(`[triage] stale-target auto-reject failed for ${id}: ${errMessage(rejectErr)}`);
|
|
99
|
+
return { message };
|
|
100
|
+
}
|
|
160
101
|
}
|
|
161
102
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
function readLiveAssetContent(stashDir, ref) {
|
|
103
|
+
/** Reject one proposal (nothing in a dry run); the error message on failure. */
|
|
104
|
+
async function rejectProposal(opts, id, reason, gateReason, rejectFn) {
|
|
105
|
+
if (opts.dryRun)
|
|
106
|
+
return undefined;
|
|
167
107
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (!fs.existsSync(assetPath))
|
|
175
|
-
return undefined;
|
|
176
|
-
return fs.readFileSync(assetPath, "utf8");
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
108
|
+
await rejectFn({
|
|
109
|
+
stashDir: opts.stashDir,
|
|
110
|
+
id,
|
|
111
|
+
reason,
|
|
112
|
+
gateDecision: { outcome: "auto-rejected", reason: gateReason, gate: DRAIN_GATE },
|
|
113
|
+
});
|
|
179
114
|
return undefined;
|
|
180
115
|
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
return errMessage(err);
|
|
118
|
+
}
|
|
181
119
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Pre-fetch the context the judgment runner needs to adjudicate one deferred
|
|
184
|
-
* proposal: the proposed content, the live asset it would overwrite, and the
|
|
185
|
-
* sibling pending proposals for the same ref (so a dedup verdict can compare).
|
|
186
|
-
*/
|
|
187
|
-
function prefetchJudgmentContext(stashDir, proposal, pending) {
|
|
188
|
-
const liveAsset = readLiveAssetContent(stashDir, proposal.ref);
|
|
189
|
-
const siblings = pending.filter((p) => p.ref === proposal.ref && p.id !== proposal.id);
|
|
190
|
-
return { liveAsset, siblings };
|
|
191
|
-
}
|
|
192
|
-
/** Build the judgment prompt with the proposed content + pre-fetched context. */
|
|
120
|
+
/** The judgment prompt: the proposal, the live asset it would overwrite, and same-ref siblings. */
|
|
193
121
|
export function buildJudgmentPrompt(proposal, reason, ctx) {
|
|
194
|
-
const proposed = proposalContent(proposal);
|
|
195
122
|
const sections = [
|
|
196
|
-
"You are adjudicating a pending knowledge-base proposal
|
|
197
|
-
"
|
|
123
|
+
"You are adjudicating a pending knowledge-base proposal no quality judge has",
|
|
124
|
+
"passed yet. Decide whether to accept, reject, or defer it.",
|
|
198
125
|
"",
|
|
199
126
|
`Asset ref: ${proposal.ref}`,
|
|
200
127
|
`Generator (source): ${proposal.source}`,
|
|
201
|
-
`
|
|
128
|
+
`Left for judgment because: ${reason === "needs-judgment" ? "no quality judge has passed this content yet" : reason}`,
|
|
202
129
|
"",
|
|
203
130
|
"## Proposed content",
|
|
204
131
|
"```",
|
|
205
|
-
|
|
132
|
+
proposalContent(proposal),
|
|
206
133
|
"```",
|
|
207
134
|
];
|
|
208
135
|
if (ctx.liveAsset !== undefined) {
|
|
@@ -220,15 +147,12 @@ export function buildJudgmentPrompt(proposal, reason, ctx) {
|
|
|
220
147
|
sections.push("", "## Your task", 'Return ONLY a JSON object: {"decision": "accept" | "reject" | "defer", "reason": "<short reason>"}.', "- accept: the proposed content is a correct, valuable update worth committing.", "- reject: the proposal is wrong, a duplicate, or contradicts the live asset.", "- defer: you cannot decide from the provided context (leave it pending).", "Output the JSON object and nothing else.");
|
|
221
148
|
return sections.join("\n");
|
|
222
149
|
}
|
|
223
|
-
/**
|
|
150
|
+
/** A verdict from raw runner output (the first JSON object), or null. */
|
|
224
151
|
export function parseJudgmentVerdict(raw) {
|
|
225
152
|
const cleaned = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw))).trim();
|
|
226
|
-
if (!cleaned)
|
|
227
|
-
return null;
|
|
228
|
-
// Find the first balanced-looking JSON object in the output.
|
|
229
153
|
const start = cleaned.indexOf("{");
|
|
230
154
|
const end = cleaned.lastIndexOf("}");
|
|
231
|
-
if (start === -1 || end
|
|
155
|
+
if (start === -1 || end <= start)
|
|
232
156
|
return null;
|
|
233
157
|
let obj;
|
|
234
158
|
try {
|
|
@@ -237,479 +161,244 @@ export function parseJudgmentVerdict(raw) {
|
|
|
237
161
|
catch {
|
|
238
162
|
return null;
|
|
239
163
|
}
|
|
240
|
-
|
|
241
|
-
return null;
|
|
242
|
-
const decision = obj.decision;
|
|
243
|
-
const reason = obj.reason;
|
|
164
|
+
const { decision, reason } = (obj ?? {});
|
|
244
165
|
if (decision !== "accept" && decision !== "reject" && decision !== "defer")
|
|
245
166
|
return null;
|
|
246
167
|
return { decision, reason: typeof reason === "string" ? reason : "" };
|
|
247
168
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
runner,
|
|
252
|
-
invocationKind: "direct",
|
|
253
|
-
});
|
|
254
|
-
const lowered = lowerResolvedExecutionRequestWithRunner(prepared.request, prepared.runner);
|
|
255
|
-
const chat = seams.chat;
|
|
256
|
-
const llmRunner = lowered.runner.kind === "llm" ? lowered.runner : undefined;
|
|
257
|
-
const dispatchOptions = {
|
|
258
|
-
lease,
|
|
259
|
-
...(seams.runAgentFn ? { runAgent: seams.runAgentFn } : {}),
|
|
260
|
-
...(seams.runSdkFn ? { runSdk: seams.runSdkFn } : {}),
|
|
261
|
-
...(chat && llmRunner
|
|
262
|
-
? {
|
|
263
|
-
chat: async (connection, messages) => chat({ ...llmRunner, connection }, messages),
|
|
264
|
-
}
|
|
265
|
-
: {}),
|
|
266
|
-
};
|
|
267
|
-
let result;
|
|
169
|
+
/** Lower the judgment prompt through the frozen runner and dispatch it. */
|
|
170
|
+
async function dispatchJudgment(runner, prompt, seams) {
|
|
171
|
+
let notices = [];
|
|
268
172
|
try {
|
|
269
|
-
|
|
173
|
+
const prepared = resolveExecution({ content: prompt, runner });
|
|
174
|
+
const lowered = buildExecution(prepared.request, prepared.runner);
|
|
175
|
+
notices = lowered.notices;
|
|
176
|
+
const chat = seams.chat;
|
|
177
|
+
const llmRunner = lowered.runner.kind === "llm" ? lowered.runner : undefined;
|
|
178
|
+
const result = await runExecution(lowered, {
|
|
179
|
+
...(seams.runAgentFn ? { runAgent: seams.runAgentFn } : {}),
|
|
180
|
+
...(seams.runSdkFn ? { runSdk: seams.runSdkFn } : {}),
|
|
181
|
+
...(chat && llmRunner
|
|
182
|
+
? { chat: async (connection, messages) => chat({ ...llmRunner, connection }, messages) }
|
|
183
|
+
: {}),
|
|
184
|
+
});
|
|
185
|
+
if (!result.ok)
|
|
186
|
+
return { verdict: null, notices, error: result.error ?? result.reason ?? "unknown error" };
|
|
187
|
+
return { verdict: parseJudgmentVerdict(result.stdout), notices };
|
|
270
188
|
}
|
|
271
189
|
catch (error) {
|
|
272
190
|
if (error instanceof ConfigError)
|
|
273
191
|
throw error;
|
|
274
|
-
return {
|
|
275
|
-
verdict: null,
|
|
276
|
-
notices: lowered.notices,
|
|
277
|
-
error: error instanceof Error ? error.message : String(error),
|
|
278
|
-
};
|
|
192
|
+
return { verdict: null, notices, error: errMessage(error) };
|
|
279
193
|
}
|
|
280
|
-
if (!result.ok) {
|
|
281
|
-
return {
|
|
282
|
-
verdict: null,
|
|
283
|
-
notices: lowered.notices,
|
|
284
|
-
error: result.error ?? result.reason ?? "unknown error",
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
return { verdict: parseJudgmentVerdict(result.stdout), notices: lowered.notices };
|
|
288
|
-
}
|
|
289
|
-
/** Validate symbolic judgment credentials without contacting a provider. */
|
|
290
|
-
async function preflightJudgmentRunner(runner) {
|
|
291
|
-
const prepared = prepareInlineExecutionWithRunner({
|
|
292
|
-
content: "Validate the selected proposal judgment runner before mutation.",
|
|
293
|
-
runner,
|
|
294
|
-
invocationKind: "direct",
|
|
295
|
-
});
|
|
296
|
-
const lowered = lowerResolvedExecutionRequestWithRunner(prepared.request, prepared.runner);
|
|
297
|
-
return acquireLoweredExecutionDispatchLease(lowered);
|
|
298
|
-
}
|
|
299
|
-
function judgedContentHash(proposal) {
|
|
300
|
-
return createHash("sha256").update(proposalContent(proposal), "utf8").digest("hex");
|
|
301
194
|
}
|
|
302
195
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* "accept" but queue mode did not promote), the ids dropped by the accept cap,
|
|
307
|
-
* and the items still unresolved (verdict "defer", parse failure, or a runner
|
|
308
|
-
* error).
|
|
196
|
+
* The judgment tier: the runner only judges; the drain performs the accept
|
|
197
|
+
* (under `applyMode` and the remaining accept budget) or the reject. A defer, an
|
|
198
|
+
* unparseable verdict or a runner error leaves the item undecided.
|
|
309
199
|
*/
|
|
310
|
-
async function runJudgmentTier(
|
|
311
|
-
const byId = new Map(
|
|
312
|
-
const
|
|
313
|
-
const rejected = [];
|
|
314
|
-
const staged = [];
|
|
315
|
-
const skippedByCap = [];
|
|
200
|
+
async function runJudgmentTier(opts, result, pending, acceptBudget, promoteFn, rejectFn, seams) {
|
|
201
|
+
const byId = new Map(pending.map((p) => [p.id, p]));
|
|
202
|
+
const notices = noticeSet();
|
|
316
203
|
const stillDeferred = [];
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
let acceptBudget = Math.max(0, input.remainingAcceptBudget);
|
|
320
|
-
for (const item of input.deferred) {
|
|
204
|
+
const cappedBefore = result.skippedByCap.length;
|
|
205
|
+
for (const item of result.deferred) {
|
|
321
206
|
const proposal = byId.get(item.id);
|
|
322
207
|
if (!proposal) {
|
|
323
208
|
stillDeferred.push(item);
|
|
324
209
|
continue;
|
|
325
210
|
}
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (err instanceof ConfigError)
|
|
334
|
-
throw err;
|
|
335
|
-
warn(`[triage] judgment dispatch failed for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
336
|
-
stillDeferred.push(item);
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
for (const notice of dispatch.notices) {
|
|
340
|
-
const key = JSON.stringify(notice);
|
|
341
|
-
if (!noticesByKey.has(key))
|
|
342
|
-
noticesByKey.set(key, notice);
|
|
343
|
-
}
|
|
344
|
-
if (dispatch.error) {
|
|
211
|
+
const prompt = buildJudgmentPrompt(proposal, item.reason, {
|
|
212
|
+
liveAsset: readLiveAssetContent(opts.stashDir, proposal.ref),
|
|
213
|
+
siblings: pending.filter((p) => p.ref === proposal.ref && p.id !== proposal.id),
|
|
214
|
+
});
|
|
215
|
+
const dispatch = await dispatchJudgment(opts.judgment, prompt, seams);
|
|
216
|
+
notices.add(dispatch.notices);
|
|
217
|
+
if (dispatch.error)
|
|
345
218
|
warn(`[triage] judgment dispatch failed for ${item.id}: ${dispatch.error}`);
|
|
346
|
-
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
const verdict = dispatch.verdict;
|
|
219
|
+
const verdict = dispatch.error ? null : dispatch.verdict;
|
|
350
220
|
if (!verdict || verdict.decision === "defer") {
|
|
351
221
|
stillDeferred.push(item);
|
|
352
222
|
continue;
|
|
353
223
|
}
|
|
354
224
|
if (verdict.decision === "reject") {
|
|
355
|
-
|
|
356
|
-
|
|
225
|
+
const failure = await rejectProposal(opts, item.id, verdict.reason || "judgment: reject", "judgment-reject", rejectFn);
|
|
226
|
+
if (failure === undefined) {
|
|
227
|
+
result.rejected.push(item.id);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
warn(`[triage] judgment reject failed for ${item.id}: ${failure}`);
|
|
231
|
+
stillDeferred.push(item);
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// Queue mode never writes the asset: the verdict is staged for a later promote run.
|
|
236
|
+
if (opts.applyMode !== "promote") {
|
|
237
|
+
if (opts.dryRun) {
|
|
238
|
+
result.staged.push(item.id);
|
|
357
239
|
continue;
|
|
358
240
|
}
|
|
359
241
|
try {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
242
|
+
recordGateDecision(opts.stashDir, item.id, {
|
|
243
|
+
outcome: "staged",
|
|
244
|
+
reason: "judgment-accept",
|
|
245
|
+
contentHash: proposalContentHash(proposal),
|
|
246
|
+
gate: DRAIN_GATE,
|
|
365
247
|
});
|
|
366
|
-
|
|
248
|
+
result.staged.push(item.id);
|
|
367
249
|
}
|
|
368
250
|
catch (err) {
|
|
369
|
-
warn(`[triage]
|
|
251
|
+
warn(`[triage] failed to stage judgment for ${item.id}: ${errMessage(err)}`);
|
|
370
252
|
stillDeferred.push(item);
|
|
371
253
|
}
|
|
372
254
|
continue;
|
|
373
255
|
}
|
|
374
|
-
// decision === "accept" — gated on applyMode, exactly like the
|
|
375
|
-
// deterministic accept path (queue mode never writes).
|
|
376
|
-
if (input.applyMode !== "promote") {
|
|
377
|
-
// Staged: a queue-mode run never promotes, so the item stays pending but
|
|
378
|
-
// is RESOLVED (the runner judged it). Track separately so it is NOT
|
|
379
|
-
// reported as "left unresolved" and a follow-up promote run picks it up.
|
|
380
|
-
staged.push(item.id);
|
|
381
|
-
if (!input.dryRun) {
|
|
382
|
-
try {
|
|
383
|
-
recordGateDecision(input.stashDir, item.id, {
|
|
384
|
-
outcome: "staged",
|
|
385
|
-
reason: "judgment-accept",
|
|
386
|
-
contentHash: judgedContentHash(proposal),
|
|
387
|
-
gate: input.gateLabel,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
catch (err) {
|
|
391
|
-
warn(`[triage] failed to stage judgment for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
392
|
-
staged.pop();
|
|
393
|
-
stillDeferred.push(item);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
// Accept cap: once the shared budget is exhausted, route further accepts to
|
|
399
|
-
// skippedByCap instead of promoting (keeps total promotions ≤ maxAccepts).
|
|
400
256
|
if (acceptBudget <= 0) {
|
|
401
|
-
skippedByCap.push(item.id);
|
|
257
|
+
result.skippedByCap.push(item.id);
|
|
402
258
|
continue;
|
|
403
259
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
preflightProposalPromotion(input.config, proposal, {
|
|
408
|
-
...(input.target ? { target: input.target } : {}),
|
|
409
|
-
gateDecision: { outcome: "auto-accepted", reason: "judgment-accept", gate: input.gateLabel },
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
catch (err) {
|
|
414
|
-
warn(`[triage] judgment preflight failed for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
415
|
-
stillDeferred.push(item);
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
promoted.push(item.id);
|
|
260
|
+
const outcome = await acceptProposal(opts, proposal, item.id, "judgment-accept", promoteFn, rejectFn);
|
|
261
|
+
if (outcome === "promoted") {
|
|
262
|
+
result.promoted.push(item.id);
|
|
419
263
|
acceptBudget -= 1;
|
|
420
|
-
continue;
|
|
421
264
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
stashDir: input.stashDir,
|
|
425
|
-
id: item.id,
|
|
426
|
-
...(input.target ? { target: input.target } : {}),
|
|
427
|
-
...(input.config ? { config: input.config } : {}),
|
|
428
|
-
gateDecision: { outcome: "auto-accepted", reason: "judgment-accept", gate: input.gateLabel },
|
|
429
|
-
});
|
|
430
|
-
promoted.push(item.id);
|
|
431
|
-
acceptBudget -= 1;
|
|
265
|
+
else if (outcome === "rejected") {
|
|
266
|
+
result.rejected.push(item.id);
|
|
432
267
|
}
|
|
433
|
-
|
|
434
|
-
warn(`[triage] judgment promote failed for ${item.id}: ${
|
|
268
|
+
else {
|
|
269
|
+
warn(`[triage] judgment ${opts.dryRun ? "preflight" : "promote"} failed for ${item.id}: ${outcome.message}`);
|
|
435
270
|
stillDeferred.push(item);
|
|
436
271
|
}
|
|
437
272
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
273
|
+
const capped = result.skippedByCap.length - cappedBefore;
|
|
274
|
+
if (capped > 0) {
|
|
275
|
+
info(`[triage] accept ceiling reached in judgment tier: ${capped} judged-accept items skipped by cap (maxAccepts=${opts.maxAccepts})`);
|
|
276
|
+
}
|
|
277
|
+
if (notices.list().length > 0)
|
|
278
|
+
result.notices = notices.list();
|
|
279
|
+
result.deferred = stillDeferred;
|
|
280
|
+
}
|
|
281
|
+
/** The live asset a proposal would overwrite, if any. */
|
|
282
|
+
function readLiveAssetContent(stashDir, ref) {
|
|
283
|
+
try {
|
|
284
|
+
const parsed = parseRefInput(ref);
|
|
285
|
+
const typeDir = stashDirFor(parsed.type);
|
|
286
|
+
if (!typeDir)
|
|
287
|
+
return undefined;
|
|
288
|
+
const assetPath = assetPathForName(parsed.type, path.join(stashDir, typeDir), parsed.name);
|
|
289
|
+
return fs.existsSync(assetPath) ? fs.readFileSync(assetPath, "utf8") : undefined;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
446
294
|
}
|
|
447
|
-
/**
|
|
448
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Drain the pending backlog. `promoteFn` / `rejectFn` / `judgmentSeams` are
|
|
297
|
+
* test seams.
|
|
298
|
+
*/
|
|
299
|
+
export async function drainProposals(opts, promoteFn = akmProposalAccept, rejectFn = akmProposalReject, judgmentSeams = {}) {
|
|
449
300
|
const exclude = opts.excludeIds ?? new Set();
|
|
450
|
-
// A
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
}).filter((proposal) => !exclude.has(proposal.id));
|
|
457
|
-
const acceptIds = [];
|
|
458
|
-
const acceptGateReasons = new Map();
|
|
459
|
-
const rejectTargets = [];
|
|
460
|
-
const deferred = [];
|
|
461
|
-
const deferredGateDecisions = [];
|
|
462
|
-
const gateLabel = `triage:${opts.policy.name}`;
|
|
463
|
-
const needsJudge = new Set();
|
|
301
|
+
// A judgment runner's credentials are validated before any live state
|
|
302
|
+
// connection, so its classification reads an isolated snapshot.
|
|
303
|
+
const pending = (opts.judgment ? listProposalsReadOnly : listProposals)(opts.stashDir, { status: "pending" }).filter((proposal) => !exclude.has(proposal.id));
|
|
304
|
+
const result = { promoted: [], rejected: [], deferred: [], skippedByCap: [], staged: [], failed: [] };
|
|
305
|
+
const accepts = [];
|
|
306
|
+
const empties = [];
|
|
464
307
|
for (const proposal of pending) {
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
|
|
308
|
+
// A consolidate pair-pass `retire` proposal is never auto-decided here,
|
|
309
|
+
// whatever `applyMode` says (alpha.9 brief §A "Review"; spec §25.6):
|
|
310
|
+
// untouched, still pending, waiting for a direct `akm proposal accept`.
|
|
311
|
+
// Checked before isEmptyDiff, which reads proposalContent() and has
|
|
312
|
+
// nothing meaningful to read on a delete-primary change anyway.
|
|
313
|
+
if (isRetireProposal(proposal))
|
|
468
314
|
continue;
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
acceptIds.push(proposal.id);
|
|
474
|
-
acceptGateReasons.set(proposal.id, "judgment-accept");
|
|
315
|
+
const decision = proposal.gateDecision;
|
|
316
|
+
// Another gate's rejection stands; a human-review deferral from the distill
|
|
317
|
+
// quality gate is left for that human.
|
|
318
|
+
if (decision?.outcome === "auto-rejected" && !decision.gate?.startsWith(DRAIN_GATE))
|
|
475
319
|
continue;
|
|
476
|
-
|
|
477
|
-
const decision = classifyProposal(proposal, opts.policy, opts.maxDiffLines);
|
|
478
|
-
if (decision === null)
|
|
320
|
+
if (decision?.outcome === "deferred" && decision.gate === "quality-gate")
|
|
479
321
|
continue;
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
id: proposal.id,
|
|
483
|
-
decision: {
|
|
484
|
-
outcome: "deferred",
|
|
485
|
-
reason: decision.gate.reason,
|
|
486
|
-
...(decision.gate.measured !== undefined ? { measured: decision.gate.measured } : {}),
|
|
487
|
-
...(decision.gate.thresholds ? { thresholds: decision.gate.thresholds } : {}),
|
|
488
|
-
gate: gateLabel,
|
|
489
|
-
},
|
|
490
|
-
});
|
|
491
|
-
if (!decision.gate.thresholds)
|
|
492
|
-
needsJudge.add(proposal.id);
|
|
493
|
-
}
|
|
494
|
-
if (decision.verdict === "accept") {
|
|
495
|
-
acceptIds.push(proposal.id);
|
|
496
|
-
acceptGateReasons.set(proposal.id, "policy-accept");
|
|
322
|
+
if (isEmptyDiff(proposal)) {
|
|
323
|
+
empties.push(proposal.id);
|
|
497
324
|
}
|
|
498
|
-
else if (decision
|
|
499
|
-
|
|
325
|
+
else if (decision?.outcome === "staged" && decision.contentHash === proposalContentHash(proposal)) {
|
|
326
|
+
accepts.push({ id: proposal.id, reason: decision.gate === "quality-gate" ? "judge-passed" : "judgment-accept" });
|
|
500
327
|
}
|
|
501
328
|
else {
|
|
502
|
-
deferred.push({ id: proposal.id, reason:
|
|
329
|
+
result.deferred.push({ id: proposal.id, reason: "needs-judgment" });
|
|
503
330
|
}
|
|
504
331
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
* Drain the standing pending backlog under a deterministic policy.
|
|
518
|
-
*
|
|
519
|
-
* @param opts Drain options (policy, applyMode, ceilings, dry-run).
|
|
520
|
-
* @param promoteFn Injectable override for `akmProposalAccept` (test seam).
|
|
521
|
-
* @param rejectFn Injectable override for `akmProposalReject` (test seam).
|
|
522
|
-
*/
|
|
523
|
-
export async function drainProposals(opts, promoteFn = akmProposalAccept, rejectFn = akmProposalReject, judgmentSeams = {}) {
|
|
524
|
-
const classification = classifyPendingProposals(opts);
|
|
525
|
-
const { pending, acceptIds, acceptGateReasons, rejectTargets, deferredGateDecisions, gateLabel, needsJudge } = classification;
|
|
526
|
-
const result = {
|
|
527
|
-
promoted: [],
|
|
528
|
-
rejected: [],
|
|
529
|
-
deferred: classification.deferred,
|
|
530
|
-
skippedByCap: [],
|
|
531
|
-
staged: [],
|
|
532
|
-
failed: [],
|
|
533
|
-
};
|
|
534
|
-
// A configured judgment runner makes every deferred item dispatch-eligible.
|
|
535
|
-
// Validate its symbolic credentials before applying any deterministic gate,
|
|
536
|
-
// reject, promote, or event mutation. Provider/runtime failures remain the
|
|
537
|
-
// judgment tier's fail-soft responsibility after this configuration fence.
|
|
538
|
-
const dispatchLease = opts.judgment && result.deferred.length > 0 ? await preflightJudgmentRunner(opts.judgment) : undefined;
|
|
539
|
-
try {
|
|
540
|
-
for (const { id, decision } of deferredGateDecisions)
|
|
541
|
-
stampGateDecision(opts, id, decision);
|
|
542
|
-
// --- Reject empties (independent of the accept ceiling / applyMode) ---
|
|
543
|
-
for (const target of rejectTargets) {
|
|
544
|
-
if (opts.dryRun) {
|
|
545
|
-
result.rejected.push(target.id);
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
try {
|
|
549
|
-
await rejectFn({
|
|
550
|
-
stashDir: opts.stashDir,
|
|
551
|
-
id: target.id,
|
|
552
|
-
reason: target.reason,
|
|
553
|
-
gateDecision: { outcome: "auto-rejected", reason: "empty-diff", gate: gateLabel },
|
|
554
|
-
});
|
|
555
|
-
result.rejected.push(target.id);
|
|
556
|
-
}
|
|
557
|
-
catch (err) {
|
|
558
|
-
const message = pushDrainFailure(result, target.id, err, "reject-error");
|
|
559
|
-
warn(`[triage] reject failed for ${target.id}: ${message}`);
|
|
560
|
-
}
|
|
332
|
+
if (opts.judgment && result.deferred.length > 0) {
|
|
333
|
+
// Symbolic credentials are checked before any gate, reject or promote.
|
|
334
|
+
const prepared = resolveExecution({
|
|
335
|
+
content: "Validate the selected proposal judgment runner before mutation.",
|
|
336
|
+
runner: opts.judgment,
|
|
337
|
+
});
|
|
338
|
+
assertRunnerCredentials(buildExecution(prepared.request, prepared.runner).runner);
|
|
339
|
+
}
|
|
340
|
+
for (const id of empties) {
|
|
341
|
+
const failure = await rejectProposal(opts, id, "empty diff", "empty-diff", rejectFn);
|
|
342
|
+
if (failure === undefined) {
|
|
343
|
+
result.rejected.push(id);
|
|
561
344
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (result.skippedByCap.length > 0) {
|
|
566
|
-
info(`[triage] accept ceiling reached: ${withinCap.length} promoted, ${result.skippedByCap.length} skipped by cap (maxAccepts=${opts.maxAccepts})`);
|
|
345
|
+
else {
|
|
346
|
+
result.failed.push({ id, reason: categorizeDrainFailure(failure, "reject-error"), detail: failure });
|
|
347
|
+
warn(`[triage] reject failed for ${id}: ${failure}`);
|
|
567
348
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
349
|
+
}
|
|
350
|
+
const cap = Math.max(0, opts.maxAccepts);
|
|
351
|
+
const withinCap = accepts.slice(0, cap);
|
|
352
|
+
result.skippedByCap = accepts.slice(cap).map((a) => a.id);
|
|
353
|
+
if (result.skippedByCap.length > 0) {
|
|
354
|
+
info(`[triage] accept ceiling reached: ${withinCap.length} promoted, ${result.skippedByCap.length} skipped by cap (maxAccepts=${opts.maxAccepts})`);
|
|
355
|
+
}
|
|
356
|
+
let promotedHere = 0;
|
|
357
|
+
if (opts.applyMode === "promote") {
|
|
358
|
+
if (!opts.dryRun)
|
|
573
359
|
info(`[triage] auto-promote active: ${withinCap.length} accepts allowed this run`);
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
...(opts.config ? { config: opts.config } : {}),
|
|
581
|
-
gateDecision: {
|
|
582
|
-
outcome: "auto-accepted",
|
|
583
|
-
reason: acceptGateReasons.get(id) ?? "policy-accept",
|
|
584
|
-
gate: gateLabel,
|
|
585
|
-
},
|
|
586
|
-
});
|
|
587
|
-
result.promoted.push(id);
|
|
588
|
-
deterministicPromoted += 1;
|
|
589
|
-
}
|
|
590
|
-
catch (err) {
|
|
591
|
-
const message = pushDrainFailure(result, id, err, "promote-error");
|
|
592
|
-
warn(`[triage] promote failed for ${id}: ${message}`);
|
|
593
|
-
}
|
|
360
|
+
const byId = new Map(pending.map((proposal) => [proposal.id, proposal]));
|
|
361
|
+
for (const { id, reason } of withinCap) {
|
|
362
|
+
const outcome = await acceptProposal(opts, byId.get(id), id, reason, promoteFn, rejectFn);
|
|
363
|
+
if (outcome === "promoted") {
|
|
364
|
+
result.promoted.push(id);
|
|
365
|
+
promotedHere += 1;
|
|
594
366
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
// Exercise the same stamped candidate, lint, and stale-target boundary as
|
|
598
|
-
// real promotion so a dry-run's predicted promotions match what a real
|
|
599
|
-
// run would do. Tests that omit config retain the classification-only seam.
|
|
600
|
-
const byId = new Map(pending.map((proposal) => [proposal.id, proposal]));
|
|
601
|
-
for (const id of withinCap) {
|
|
602
|
-
try {
|
|
603
|
-
if (opts.config) {
|
|
604
|
-
const proposal = byId.get(id);
|
|
605
|
-
if (!proposal)
|
|
606
|
-
throw new Error(`Proposal ${id} disappeared during drain preflight.`);
|
|
607
|
-
const preflight = preflightProposalPromotion(opts.config, proposal, {
|
|
608
|
-
...(opts.target ? { target: opts.target } : {}),
|
|
609
|
-
gateDecision: {
|
|
610
|
-
outcome: "auto-accepted",
|
|
611
|
-
reason: acceptGateReasons.get(id) ?? "policy-accept",
|
|
612
|
-
gate: gateLabel,
|
|
613
|
-
},
|
|
614
|
-
});
|
|
615
|
-
assertProposalTargetFresh(proposal, preflight.assetPath);
|
|
616
|
-
}
|
|
617
|
-
result.promoted.push(id);
|
|
618
|
-
deterministicPromoted += 1;
|
|
619
|
-
}
|
|
620
|
-
catch (err) {
|
|
621
|
-
const message = pushDrainFailure(result, id, err, "promote-error");
|
|
622
|
-
warn(`[triage] preflight failed for ${id}: ${message}`);
|
|
623
|
-
}
|
|
367
|
+
else if (outcome === "rejected") {
|
|
368
|
+
result.rejected.push(id);
|
|
624
369
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
// Only runs when a RunnerSpec is configured. The runner returns a verdict; the
|
|
633
|
-
// ENGINE performs the resulting accept (respecting applyMode) / reject write.
|
|
634
|
-
if (opts.judgment && result.deferred.length > 0) {
|
|
635
|
-
if (!dispatchLease)
|
|
636
|
-
throw new TypeError("proposal judgment work requires an operation dispatch lease");
|
|
637
|
-
const tier = await runJudgmentTier({
|
|
638
|
-
stashDir: opts.stashDir,
|
|
639
|
-
applyMode: opts.applyMode,
|
|
640
|
-
dryRun: opts.dryRun,
|
|
641
|
-
runner: opts.judgment,
|
|
642
|
-
lease: dispatchLease,
|
|
643
|
-
deferred: result.deferred,
|
|
644
|
-
pending,
|
|
645
|
-
promoteFn,
|
|
646
|
-
rejectFn,
|
|
647
|
-
seams: judgmentSeams,
|
|
648
|
-
...(opts.target ? { target: opts.target } : {}),
|
|
649
|
-
...(opts.config ? { config: opts.config } : {}),
|
|
650
|
-
remainingAcceptBudget,
|
|
651
|
-
gateLabel,
|
|
652
|
-
});
|
|
653
|
-
result.promoted.push(...tier.promoted);
|
|
654
|
-
result.rejected.push(...tier.rejected);
|
|
655
|
-
result.staged.push(...tier.staged);
|
|
656
|
-
if (tier.notices.length > 0)
|
|
657
|
-
result.notices = tier.notices;
|
|
658
|
-
// Judgment-tier accepts dropped by the shared accept cap surface under
|
|
659
|
-
// skippedByCap, same as deterministic cap drops.
|
|
660
|
-
result.skippedByCap.push(...tier.skippedByCap);
|
|
661
|
-
if (tier.skippedByCap.length > 0) {
|
|
662
|
-
info(`[triage] accept ceiling reached in judgment tier: ${tier.skippedByCap.length} judged-accept items skipped by cap (maxAccepts=${opts.maxAccepts})`);
|
|
663
|
-
}
|
|
664
|
-
// Replace the deferred list with only the items the judgment tier could NOT
|
|
665
|
-
// resolve (verdict "defer", parse failure, or runner error). Staged
|
|
666
|
-
// queue-mode accepts are RESOLVED and tracked in result.staged instead.
|
|
667
|
-
result.deferred = tier.stillDeferred;
|
|
668
|
-
}
|
|
669
|
-
else if (result.deferred.length > 0) {
|
|
670
|
-
// #577: no judgment runner configured — items deferred *because they need a
|
|
671
|
-
// judge* (mid-band / possible-dup, no threshold reason) stay pending solely
|
|
672
|
-
// for lack of one. Re-stamp those as `no-judge-configured` so the operator
|
|
673
|
-
// sees a per-proposal reason instead of inferring it from the run-level
|
|
674
|
-
// triage_deferred aggregate. Band-deferred items keep their specific reason
|
|
675
|
-
// (e.g. `max-diff-lines`), which is more actionable than "no judge".
|
|
676
|
-
for (const item of result.deferred) {
|
|
677
|
-
if (needsJudge.has(item.id)) {
|
|
678
|
-
stampGateDecision(opts, item.id, { outcome: "deferred", reason: "no-judge-configured", gate: gateLabel });
|
|
679
|
-
}
|
|
370
|
+
else {
|
|
371
|
+
result.failed.push({
|
|
372
|
+
id,
|
|
373
|
+
reason: categorizeDrainFailure(outcome.message, "promote-error"),
|
|
374
|
+
detail: outcome.message,
|
|
375
|
+
});
|
|
376
|
+
warn(`[triage] ${opts.dryRun ? "preflight" : "promote"} failed for ${id}: ${outcome.message}`);
|
|
680
377
|
}
|
|
681
378
|
}
|
|
682
|
-
emitDrainEvents(opts, result);
|
|
683
|
-
return result;
|
|
684
379
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
disposeLoweredExecutionDispatchLease(dispatchLease);
|
|
380
|
+
if (opts.judgment && result.deferred.length > 0) {
|
|
381
|
+
await runJudgmentTier({ ...opts, judgment: opts.judgment }, result, pending, cap - promotedHere, promoteFn, rejectFn, judgmentSeams);
|
|
688
382
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
catch (err) {
|
|
702
|
-
warn(`[triage] failed to record gate decision for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
383
|
+
// #577: whatever stays undecided is left for review (`review_needed` in the ledger).
|
|
384
|
+
if (!opts.dryRun) {
|
|
385
|
+
const reviewReason = opts.judgment ? "judgment-deferred" : "no-judge-configured";
|
|
386
|
+
for (const item of result.deferred) {
|
|
387
|
+
try {
|
|
388
|
+
recordGateDecision(opts.stashDir, item.id, { outcome: "deferred", reason: reviewReason, gate: DRAIN_GATE });
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
warn(`[triage] failed to record gate decision for ${item.id}: ${errMessage(err)}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
703
394
|
}
|
|
395
|
+
emitDrainEvents(opts, result);
|
|
396
|
+
return result;
|
|
704
397
|
}
|
|
705
|
-
// ---------------------------------------------------------------------------
|
|
706
|
-
// Events
|
|
707
|
-
// ---------------------------------------------------------------------------
|
|
708
398
|
function emitDrainEvents(opts, result) {
|
|
709
399
|
const deferredByReason = {};
|
|
710
|
-
for (const d of result.deferred)
|
|
400
|
+
for (const d of result.deferred)
|
|
711
401
|
deferredByReason[d.reason] = (deferredByReason[d.reason] ?? 0) + 1;
|
|
712
|
-
}
|
|
713
402
|
appendEvent({
|
|
714
403
|
eventType: "triage_drained",
|
|
715
404
|
metadata: {
|
|
@@ -718,17 +407,11 @@ function emitDrainEvents(opts, result) {
|
|
|
718
407
|
deferredByReason,
|
|
719
408
|
skippedByCap: result.skippedByCap.length,
|
|
720
409
|
...(result.staged.length > 0 ? { staged: result.staged.length } : {}),
|
|
721
|
-
policy: opts.policy.name,
|
|
722
410
|
applyMode: opts.applyMode,
|
|
723
411
|
...(opts.dryRun ? { dryRun: true } : {}),
|
|
724
412
|
},
|
|
725
413
|
}, opts.eventsCtx ?? {});
|
|
726
|
-
//
|
|
727
|
-
// tier so a backlog of deferred items never silently looks like full success.
|
|
728
|
-
// This fires when no runner is configured OR the judgment tier ran but could
|
|
729
|
-
// not resolve every item (verdict "defer", parse failure, or a runner error).
|
|
730
|
-
// Queue-mode staged accepts are RESOLVED (the judge decided) and live in
|
|
731
|
-
// result.staged, so they are deliberately excluded from this "unresolved" count.
|
|
414
|
+
// Undecided items must never look like full success. Staged accepts were decided.
|
|
732
415
|
if (result.deferred.length > 0) {
|
|
733
416
|
appendEvent({
|
|
734
417
|
eventType: "triage_deferred",
|