akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4
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 +756 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- 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 +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- 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 +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- 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 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- 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 +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- 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/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- 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 +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- 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-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- 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 +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- 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/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- 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 +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -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 +2 -7
- 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/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.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 +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- 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 +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- 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/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 +0 -17
- 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 +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- 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 +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- 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 +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- 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/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +363 -679
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- 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 +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- 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 +87 -406
- 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 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- 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 -21
- 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/config/retired-experimental-keys-shim.js +0 -62
- 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/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- 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/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/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 -78
- 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,134 +2,46 @@
|
|
|
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";
|
|
41
|
-
import {
|
|
22
|
+
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
42
23
|
import { parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
43
24
|
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
32
|
import { STALE_TARGET_GATE_REASON } from "./proposal-types.js";
|
|
51
|
-
import { listProposals, listProposalsReadOnly, preflightProposalPromotion, proposalContent, recordGateDecision, } from "./repository.js";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/** Number of non-empty body lines (frontmatter excluded). */
|
|
56
|
-
export function contentBodyLineCount(content) {
|
|
57
|
-
// Reuse the canonical frontmatter parser so CRLF / BOM are handled
|
|
58
|
-
// consistently with the rest of the stash (parseFrontmatter returns the body
|
|
59
|
-
// in `content`).
|
|
60
|
-
return parseFrontmatter(content)
|
|
61
|
-
.content.split("\n")
|
|
62
|
-
.filter((line) => line.trim().length > 0).length;
|
|
63
|
-
}
|
|
64
|
-
/** Total line count of the proposed content (matches the bulk-accept measure). */
|
|
65
|
-
export function contentLineCount(content) {
|
|
66
|
-
return content.split("\n").length;
|
|
67
|
-
}
|
|
68
|
-
/** An empty / near-empty diff has no meaningful body content. */
|
|
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. */
|
|
69
37
|
export function isEmptyDiff(proposal) {
|
|
70
38
|
const content = proposalContent(proposal);
|
|
71
39
|
if (content.trim().length === 0)
|
|
72
40
|
return true;
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
* Decide a deterministic verdict for a single backlog proposal under `policy`.
|
|
77
|
-
* Returns `null` when no rule applies (the proposal is left pending untouched).
|
|
78
|
-
*/
|
|
79
|
-
export function classifyProposal(proposal, policy, maxDiffLines) {
|
|
80
|
-
const content = proposalContent(proposal);
|
|
81
|
-
// Empty / near-empty diffs reject first (the reject-empty floor).
|
|
82
|
-
if (policy.rejectEmpty && isEmptyDiff(proposal)) {
|
|
83
|
-
return { verdict: "reject", reason: "empty diff", gate: { reason: "empty-diff" } };
|
|
84
|
-
}
|
|
85
|
-
const rule = policy.accept.find((r) => {
|
|
86
|
-
if (r.generator !== proposal.source)
|
|
87
|
-
return false;
|
|
88
|
-
if (r.requireType !== undefined) {
|
|
89
|
-
const fm = parseFrontmatter(proposalContent(proposal)).data;
|
|
90
|
-
if (typeof fm.type !== "string" || fm.type !== r.requireType)
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
return true;
|
|
94
|
-
});
|
|
95
|
-
if (rule) {
|
|
96
|
-
const lines = contentLineCount(content);
|
|
97
|
-
const body = contentBodyLineCount(content);
|
|
98
|
-
// Per-rule and global diff bounds defer large accepts (no silent rewrites).
|
|
99
|
-
const effectiveMax = Math.min(rule.maxDiffLines ?? Number.POSITIVE_INFINITY, maxDiffLines ?? Number.POSITIVE_INFINITY);
|
|
100
|
-
if (lines > effectiveMax) {
|
|
101
|
-
return {
|
|
102
|
-
verdict: "defer",
|
|
103
|
-
reason: "mid-band",
|
|
104
|
-
gate: { reason: "max-diff-lines", measured: lines, thresholds: { maxDiffLines: effectiveMax } },
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
if (rule.minContentLines !== undefined && body < rule.minContentLines) {
|
|
108
|
-
// Too little content to confidently auto-accept — leave for judgment.
|
|
109
|
-
return {
|
|
110
|
-
verdict: "defer",
|
|
111
|
-
reason: "mid-band",
|
|
112
|
-
gate: { reason: "min-content-lines", measured: body, thresholds: { minContentLines: rule.minContentLines } },
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
return { verdict: "accept", gate: { reason: "policy-accept" } };
|
|
116
|
-
}
|
|
117
|
-
if (policy.defer.includes(proposal.source)) {
|
|
118
|
-
const reason = deferReasonForSource(proposal.source);
|
|
119
|
-
return { verdict: "defer", reason, gate: { reason } };
|
|
120
|
-
}
|
|
121
|
-
// No matching rule — leave pending, untouched.
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
function deferReasonForSource(source) {
|
|
125
|
-
return source === "distill" ? "possible-dup" : "mid-band";
|
|
41
|
+
return !parseFrontmatter(content)
|
|
42
|
+
.content.split("\n")
|
|
43
|
+
.some((line) => line.trim().length > 0);
|
|
126
44
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Map a thrown error's message to one of `DrainResult.failed`'s stable reason
|
|
129
|
-
* codes, falling back to `fallback` for anything not specifically recognized.
|
|
130
|
-
* Recognizes the write-time guards a proposal can trip during promotion
|
|
131
|
-
* (see repository.ts's `promoteProposalWithLease` / `preflightProposalPromotion`).
|
|
132
|
-
*/
|
|
133
45
|
function categorizeDrainFailure(message, fallback) {
|
|
134
46
|
if (/target (?:changed after|was created after) proposal/.test(message))
|
|
135
47
|
return STALE_TARGET_GATE_REASON;
|
|
@@ -137,108 +49,87 @@ function categorizeDrainFailure(message, fallback) {
|
|
|
137
49
|
return "validation";
|
|
138
50
|
return fallback;
|
|
139
51
|
}
|
|
140
|
-
function pushDrainFailure(result, id, err, fallbackReason) {
|
|
141
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
-
result.failed.push({ id, reason: categorizeDrainFailure(message, fallbackReason), detail: message });
|
|
143
|
-
return message;
|
|
144
|
-
}
|
|
145
52
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* instead of
|
|
150
|
-
* the
|
|
151
|
-
* `checkFingerprintAndBackoff` (repository.ts) excludes this reason from
|
|
152
|
-
* rejection-backoff, so the ref stays re-proposable against its current
|
|
153
|
-
* content. Returns `true` when the reject succeeded (the caller should treat
|
|
154
|
-
* the item as resolved, not failed); `false` leaves it to the caller's
|
|
155
|
-
* existing failure handling.
|
|
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.
|
|
156
58
|
*/
|
|
157
|
-
async function
|
|
59
|
+
async function acceptProposal(opts, proposal, id, reason, promoteFn, rejectFn) {
|
|
60
|
+
const gateDecision = { outcome: "auto-accepted", reason, gate: DRAIN_GATE };
|
|
158
61
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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";
|
|
166
81
|
}
|
|
167
82
|
catch (err) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
? backup !== undefined &&
|
|
186
|
-
computeNormalizedContentHash(backup.toString("utf8")) === proposal.beforeHashNormalized
|
|
187
|
-
: backup !== undefined && createHash("sha256").update(backup).digest("hex") === proposal.beforeHash;
|
|
188
|
-
if (!fresh) {
|
|
189
|
-
throw new Error(`Proposal target changed after proposal ${proposal.id} was created; refusing to overwrite newer content.`);
|
|
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 };
|
|
190
100
|
}
|
|
191
|
-
}
|
|
192
|
-
if (proposal.beforeHash === undefined &&
|
|
193
|
-
backup !== undefined &&
|
|
194
|
-
proposal.changes.some((change) => change.op === "create")) {
|
|
195
|
-
throw new Error(`Proposal target was created after proposal ${proposal.id} was created; refusing to overwrite newer content.`);
|
|
196
101
|
}
|
|
197
102
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
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;
|
|
203
107
|
try {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (!fs.existsSync(assetPath))
|
|
211
|
-
return undefined;
|
|
212
|
-
return fs.readFileSync(assetPath, "utf8");
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
108
|
+
await rejectFn({
|
|
109
|
+
stashDir: opts.stashDir,
|
|
110
|
+
id,
|
|
111
|
+
reason,
|
|
112
|
+
gateDecision: { outcome: "auto-rejected", reason: gateReason, gate: DRAIN_GATE },
|
|
113
|
+
});
|
|
215
114
|
return undefined;
|
|
216
115
|
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
return errMessage(err);
|
|
118
|
+
}
|
|
217
119
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Pre-fetch the context the judgment runner needs to adjudicate one deferred
|
|
220
|
-
* proposal: the proposed content, the live asset it would overwrite, and the
|
|
221
|
-
* sibling pending proposals for the same ref (so a dedup verdict can compare).
|
|
222
|
-
*/
|
|
223
|
-
function prefetchJudgmentContext(stashDir, proposal, pending) {
|
|
224
|
-
const liveAsset = readLiveAssetContent(stashDir, proposal.ref);
|
|
225
|
-
const siblings = pending.filter((p) => p.ref === proposal.ref && p.id !== proposal.id);
|
|
226
|
-
return { liveAsset, siblings };
|
|
227
|
-
}
|
|
228
|
-
/** 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. */
|
|
229
121
|
export function buildJudgmentPrompt(proposal, reason, ctx) {
|
|
230
|
-
const proposed = proposalContent(proposal);
|
|
231
122
|
const sections = [
|
|
232
|
-
"You are adjudicating a pending knowledge-base proposal
|
|
233
|
-
"
|
|
123
|
+
"You are adjudicating a pending knowledge-base proposal no quality judge has",
|
|
124
|
+
"passed yet. Decide whether to accept, reject, or defer it.",
|
|
234
125
|
"",
|
|
235
126
|
`Asset ref: ${proposal.ref}`,
|
|
236
127
|
`Generator (source): ${proposal.source}`,
|
|
237
|
-
`
|
|
128
|
+
`Left for judgment because: ${reason === "needs-judgment" ? "no quality judge has passed this content yet" : reason}`,
|
|
238
129
|
"",
|
|
239
130
|
"## Proposed content",
|
|
240
131
|
"```",
|
|
241
|
-
|
|
132
|
+
proposalContent(proposal),
|
|
242
133
|
"```",
|
|
243
134
|
];
|
|
244
135
|
if (ctx.liveAsset !== undefined) {
|
|
@@ -256,15 +147,12 @@ export function buildJudgmentPrompt(proposal, reason, ctx) {
|
|
|
256
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.");
|
|
257
148
|
return sections.join("\n");
|
|
258
149
|
}
|
|
259
|
-
/**
|
|
150
|
+
/** A verdict from raw runner output (the first JSON object), or null. */
|
|
260
151
|
export function parseJudgmentVerdict(raw) {
|
|
261
152
|
const cleaned = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw))).trim();
|
|
262
|
-
if (!cleaned)
|
|
263
|
-
return null;
|
|
264
|
-
// Find the first balanced-looking JSON object in the output.
|
|
265
153
|
const start = cleaned.indexOf("{");
|
|
266
154
|
const end = cleaned.lastIndexOf("}");
|
|
267
|
-
if (start === -1 || end
|
|
155
|
+
if (start === -1 || end <= start)
|
|
268
156
|
return null;
|
|
269
157
|
let obj;
|
|
270
158
|
try {
|
|
@@ -273,512 +161,237 @@ export function parseJudgmentVerdict(raw) {
|
|
|
273
161
|
catch {
|
|
274
162
|
return null;
|
|
275
163
|
}
|
|
276
|
-
|
|
277
|
-
return null;
|
|
278
|
-
const decision = obj.decision;
|
|
279
|
-
const reason = obj.reason;
|
|
164
|
+
const { decision, reason } = (obj ?? {});
|
|
280
165
|
if (decision !== "accept" && decision !== "reject" && decision !== "defer")
|
|
281
166
|
return null;
|
|
282
167
|
return { decision, reason: typeof reason === "string" ? reason : "" };
|
|
283
168
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
runner,
|
|
288
|
-
invocationKind: "direct",
|
|
289
|
-
});
|
|
290
|
-
const lowered = lowerResolvedExecutionRequestWithRunner(prepared.request, prepared.runner);
|
|
291
|
-
const chat = seams.chat;
|
|
292
|
-
const llmRunner = lowered.runner.kind === "llm" ? lowered.runner : undefined;
|
|
293
|
-
const dispatchOptions = {
|
|
294
|
-
lease,
|
|
295
|
-
...(seams.runAgentFn ? { runAgent: seams.runAgentFn } : {}),
|
|
296
|
-
...(seams.runSdkFn ? { runSdk: seams.runSdkFn } : {}),
|
|
297
|
-
...(chat && llmRunner
|
|
298
|
-
? {
|
|
299
|
-
chat: async (connection, messages) => chat({ ...llmRunner, connection }, messages),
|
|
300
|
-
}
|
|
301
|
-
: {}),
|
|
302
|
-
};
|
|
303
|
-
let result;
|
|
169
|
+
/** Lower the judgment prompt through the frozen runner and dispatch it. */
|
|
170
|
+
async function dispatchJudgment(runner, prompt, seams) {
|
|
171
|
+
let notices = [];
|
|
304
172
|
try {
|
|
305
|
-
|
|
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 };
|
|
306
188
|
}
|
|
307
189
|
catch (error) {
|
|
308
190
|
if (error instanceof ConfigError)
|
|
309
191
|
throw error;
|
|
310
|
-
return {
|
|
311
|
-
verdict: null,
|
|
312
|
-
notices: lowered.notices,
|
|
313
|
-
error: error instanceof Error ? error.message : String(error),
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
if (!result.ok) {
|
|
317
|
-
return {
|
|
318
|
-
verdict: null,
|
|
319
|
-
notices: lowered.notices,
|
|
320
|
-
error: result.error ?? result.reason ?? "unknown error",
|
|
321
|
-
};
|
|
192
|
+
return { verdict: null, notices, error: errMessage(error) };
|
|
322
193
|
}
|
|
323
|
-
return { verdict: parseJudgmentVerdict(result.stdout), notices: lowered.notices };
|
|
324
|
-
}
|
|
325
|
-
/** Validate symbolic judgment credentials without contacting a provider. */
|
|
326
|
-
async function preflightJudgmentRunner(runner) {
|
|
327
|
-
const prepared = prepareInlineExecutionWithRunner({
|
|
328
|
-
content: "Validate the selected proposal judgment runner before mutation.",
|
|
329
|
-
runner,
|
|
330
|
-
invocationKind: "direct",
|
|
331
|
-
});
|
|
332
|
-
const lowered = lowerResolvedExecutionRequestWithRunner(prepared.request, prepared.runner);
|
|
333
|
-
return acquireLoweredExecutionDispatchLease(lowered);
|
|
334
|
-
}
|
|
335
|
-
function judgedContentHash(proposal) {
|
|
336
|
-
return createHash("sha256").update(proposalContent(proposal), "utf8").digest("hex");
|
|
337
194
|
}
|
|
338
195
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* "accept" but queue mode did not promote), the ids dropped by the accept cap,
|
|
343
|
-
* and the items still unresolved (verdict "defer", parse failure, or a runner
|
|
344
|
-
* 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.
|
|
345
199
|
*/
|
|
346
|
-
async function runJudgmentTier(
|
|
347
|
-
const byId = new Map(
|
|
348
|
-
const
|
|
349
|
-
const rejected = [];
|
|
350
|
-
const staged = [];
|
|
351
|
-
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();
|
|
352
203
|
const stillDeferred = [];
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
let acceptBudget = Math.max(0, input.remainingAcceptBudget);
|
|
356
|
-
for (const item of input.deferred) {
|
|
204
|
+
const cappedBefore = result.skippedByCap.length;
|
|
205
|
+
for (const item of result.deferred) {
|
|
357
206
|
const proposal = byId.get(item.id);
|
|
358
207
|
if (!proposal) {
|
|
359
208
|
stillDeferred.push(item);
|
|
360
209
|
continue;
|
|
361
210
|
}
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (err instanceof ConfigError)
|
|
370
|
-
throw err;
|
|
371
|
-
warn(`[triage] judgment dispatch failed for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
372
|
-
stillDeferred.push(item);
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
for (const notice of dispatch.notices) {
|
|
376
|
-
const key = JSON.stringify(notice);
|
|
377
|
-
if (!noticesByKey.has(key))
|
|
378
|
-
noticesByKey.set(key, notice);
|
|
379
|
-
}
|
|
380
|
-
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)
|
|
381
218
|
warn(`[triage] judgment dispatch failed for ${item.id}: ${dispatch.error}`);
|
|
382
|
-
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
const verdict = dispatch.verdict;
|
|
219
|
+
const verdict = dispatch.error ? null : dispatch.verdict;
|
|
386
220
|
if (!verdict || verdict.decision === "defer") {
|
|
387
221
|
stillDeferred.push(item);
|
|
388
222
|
continue;
|
|
389
223
|
}
|
|
390
224
|
if (verdict.decision === "reject") {
|
|
391
|
-
|
|
392
|
-
|
|
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);
|
|
393
239
|
continue;
|
|
394
240
|
}
|
|
395
241
|
try {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
242
|
+
recordGateDecision(opts.stashDir, item.id, {
|
|
243
|
+
outcome: "staged",
|
|
244
|
+
reason: "judgment-accept",
|
|
245
|
+
contentHash: proposalContentHash(proposal),
|
|
246
|
+
gate: DRAIN_GATE,
|
|
401
247
|
});
|
|
402
|
-
|
|
248
|
+
result.staged.push(item.id);
|
|
403
249
|
}
|
|
404
250
|
catch (err) {
|
|
405
|
-
warn(`[triage]
|
|
251
|
+
warn(`[triage] failed to stage judgment for ${item.id}: ${errMessage(err)}`);
|
|
406
252
|
stillDeferred.push(item);
|
|
407
253
|
}
|
|
408
254
|
continue;
|
|
409
255
|
}
|
|
410
|
-
// decision === "accept" — gated on applyMode, exactly like the
|
|
411
|
-
// deterministic accept path (queue mode never writes).
|
|
412
|
-
if (input.applyMode !== "promote") {
|
|
413
|
-
// Staged: a queue-mode run never promotes, so the item stays pending but
|
|
414
|
-
// is RESOLVED (the runner judged it). Track separately so it is NOT
|
|
415
|
-
// reported as "left unresolved" and a follow-up promote run picks it up.
|
|
416
|
-
staged.push(item.id);
|
|
417
|
-
if (!input.dryRun) {
|
|
418
|
-
try {
|
|
419
|
-
recordGateDecision(input.stashDir, item.id, {
|
|
420
|
-
outcome: "staged",
|
|
421
|
-
reason: "judgment-accept",
|
|
422
|
-
contentHash: judgedContentHash(proposal),
|
|
423
|
-
gate: input.gateLabel,
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
catch (err) {
|
|
427
|
-
warn(`[triage] failed to stage judgment for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
428
|
-
staged.pop();
|
|
429
|
-
stillDeferred.push(item);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
// Accept cap: once the shared budget is exhausted, route further accepts to
|
|
435
|
-
// skippedByCap instead of promoting (keeps total promotions ≤ maxAccepts).
|
|
436
256
|
if (acceptBudget <= 0) {
|
|
437
|
-
skippedByCap.push(item.id);
|
|
257
|
+
result.skippedByCap.push(item.id);
|
|
438
258
|
continue;
|
|
439
259
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const preflight = preflightProposalPromotion(input.config, proposal, {
|
|
444
|
-
...(input.target ? { target: input.target } : {}),
|
|
445
|
-
gateDecision: { outcome: "auto-accepted", reason: "judgment-accept", gate: input.gateLabel },
|
|
446
|
-
});
|
|
447
|
-
assertProposalTargetFresh(proposal, preflight.assetPath);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
catch (err) {
|
|
451
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
452
|
-
if (categorizeDrainFailure(message, "promote-error") === STALE_TARGET_GATE_REASON) {
|
|
453
|
-
rejected.push(item.id);
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
warn(`[triage] judgment preflight failed for ${item.id}: ${message}`);
|
|
457
|
-
stillDeferred.push(item);
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
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);
|
|
461
263
|
acceptBudget -= 1;
|
|
462
|
-
continue;
|
|
463
264
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
stashDir: input.stashDir,
|
|
467
|
-
id: item.id,
|
|
468
|
-
...(input.target ? { target: input.target } : {}),
|
|
469
|
-
...(input.config ? { config: input.config } : {}),
|
|
470
|
-
gateDecision: { outcome: "auto-accepted", reason: "judgment-accept", gate: input.gateLabel },
|
|
471
|
-
});
|
|
472
|
-
promoted.push(item.id);
|
|
473
|
-
acceptBudget -= 1;
|
|
265
|
+
else if (outcome === "rejected") {
|
|
266
|
+
result.rejected.push(item.id);
|
|
474
267
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (categorizeDrainFailure(message, "promote-error") === STALE_TARGET_GATE_REASON &&
|
|
478
|
-
(await autoRejectStaleTarget(input.stashDir, input.gateLabel, item.id, message, input.rejectFn))) {
|
|
479
|
-
rejected.push(item.id);
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
warn(`[triage] judgment promote failed for ${item.id}: ${message}`);
|
|
268
|
+
else {
|
|
269
|
+
warn(`[triage] judgment ${opts.dryRun ? "preflight" : "promote"} failed for ${item.id}: ${outcome.message}`);
|
|
483
270
|
stillDeferred.push(item);
|
|
484
271
|
}
|
|
485
272
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
+
}
|
|
494
294
|
}
|
|
495
|
-
/**
|
|
496
|
-
|
|
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 = {}) {
|
|
497
300
|
const exclude = opts.excludeIds ?? new Set();
|
|
498
|
-
// A
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
}).filter((proposal) => !exclude.has(proposal.id));
|
|
505
|
-
const acceptIds = [];
|
|
506
|
-
const acceptGateReasons = new Map();
|
|
507
|
-
const rejectTargets = [];
|
|
508
|
-
const deferred = [];
|
|
509
|
-
const deferredGateDecisions = [];
|
|
510
|
-
const gateLabel = `triage:${opts.policy.name}`;
|
|
511
|
-
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 = [];
|
|
512
307
|
for (const proposal of pending) {
|
|
513
|
-
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
// REVIEW: a `review_needed` distill/promote-memory row is stamped
|
|
519
|
-
// `deferred`/`quality-gate` by `writeQualityRejection` (distill/quality-gate.ts)
|
|
520
|
-
// precisely because the quality judge could not decide and wants a human,
|
|
521
|
-
// not the judgment tier, to see it. Skip it here — before `classifyProposal`
|
|
522
|
-
// would otherwise defer it to the judgment tier (which can auto-accept
|
|
523
|
-
// under `applyMode: promote`) and before the policy-deferred re-stamp loop
|
|
524
|
-
// in `drainProposals` would overwrite this stamp with a `triage:` one.
|
|
525
|
-
if (proposal.gateDecision?.outcome === "deferred" && proposal.gateDecision.gate === "quality-gate") {
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
if (proposal.gateDecision?.outcome === "staged" &&
|
|
529
|
-
proposal.gateDecision.gate === gateLabel &&
|
|
530
|
-
proposal.gateDecision.contentHash === judgedContentHash(proposal)) {
|
|
531
|
-
acceptIds.push(proposal.id);
|
|
532
|
-
acceptGateReasons.set(proposal.id, "judgment-accept");
|
|
308
|
+
const decision = proposal.gateDecision;
|
|
309
|
+
// Another gate's rejection stands; a human-review deferral from the distill
|
|
310
|
+
// quality gate is left for that human.
|
|
311
|
+
if (decision?.outcome === "auto-rejected" && !decision.gate?.startsWith(DRAIN_GATE))
|
|
533
312
|
continue;
|
|
534
|
-
|
|
535
|
-
const decision = classifyProposal(proposal, opts.policy, opts.maxDiffLines);
|
|
536
|
-
if (decision === null)
|
|
313
|
+
if (decision?.outcome === "deferred" && decision.gate === "quality-gate")
|
|
537
314
|
continue;
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
id: proposal.id,
|
|
541
|
-
decision: {
|
|
542
|
-
outcome: "deferred",
|
|
543
|
-
reason: decision.gate.reason,
|
|
544
|
-
...(decision.gate.measured !== undefined ? { measured: decision.gate.measured } : {}),
|
|
545
|
-
...(decision.gate.thresholds ? { thresholds: decision.gate.thresholds } : {}),
|
|
546
|
-
gate: gateLabel,
|
|
547
|
-
},
|
|
548
|
-
});
|
|
549
|
-
if (!decision.gate.thresholds)
|
|
550
|
-
needsJudge.add(proposal.id);
|
|
315
|
+
if (isEmptyDiff(proposal)) {
|
|
316
|
+
empties.push(proposal.id);
|
|
551
317
|
}
|
|
552
|
-
if (decision
|
|
553
|
-
|
|
554
|
-
acceptGateReasons.set(proposal.id, "policy-accept");
|
|
555
|
-
}
|
|
556
|
-
else if (decision.verdict === "reject") {
|
|
557
|
-
rejectTargets.push({ id: proposal.id, reason: decision.reason });
|
|
318
|
+
else if (decision?.outcome === "staged" && decision.contentHash === proposalContentHash(proposal)) {
|
|
319
|
+
accepts.push({ id: proposal.id, reason: decision.gate === "quality-gate" ? "judge-passed" : "judgment-accept" });
|
|
558
320
|
}
|
|
559
321
|
else {
|
|
560
|
-
deferred.push({ id: proposal.id, reason:
|
|
322
|
+
result.deferred.push({ id: proposal.id, reason: "needs-judgment" });
|
|
561
323
|
}
|
|
562
324
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
* Drain the standing pending backlog under a deterministic policy.
|
|
576
|
-
*
|
|
577
|
-
* @param opts Drain options (policy, applyMode, ceilings, dry-run).
|
|
578
|
-
* @param promoteFn Injectable override for `akmProposalAccept` (test seam).
|
|
579
|
-
* @param rejectFn Injectable override for `akmProposalReject` (test seam).
|
|
580
|
-
*/
|
|
581
|
-
export async function drainProposals(opts, promoteFn = akmProposalAccept, rejectFn = akmProposalReject, judgmentSeams = {}) {
|
|
582
|
-
const classification = classifyPendingProposals(opts);
|
|
583
|
-
const { pending, acceptIds, acceptGateReasons, rejectTargets, deferredGateDecisions, gateLabel, needsJudge } = classification;
|
|
584
|
-
const result = {
|
|
585
|
-
promoted: [],
|
|
586
|
-
rejected: [],
|
|
587
|
-
deferred: classification.deferred,
|
|
588
|
-
skippedByCap: [],
|
|
589
|
-
staged: [],
|
|
590
|
-
failed: [],
|
|
591
|
-
};
|
|
592
|
-
// A configured judgment runner makes every deferred item dispatch-eligible.
|
|
593
|
-
// Validate its symbolic credentials before applying any deterministic gate,
|
|
594
|
-
// reject, promote, or event mutation. Provider/runtime failures remain the
|
|
595
|
-
// judgment tier's fail-soft responsibility after this configuration fence.
|
|
596
|
-
const dispatchLease = opts.judgment && result.deferred.length > 0 ? await preflightJudgmentRunner(opts.judgment) : undefined;
|
|
597
|
-
try {
|
|
598
|
-
for (const { id, decision } of deferredGateDecisions)
|
|
599
|
-
stampGateDecision(opts, id, decision);
|
|
600
|
-
// --- Reject empties (independent of the accept ceiling / applyMode) ---
|
|
601
|
-
for (const target of rejectTargets) {
|
|
602
|
-
if (opts.dryRun) {
|
|
603
|
-
result.rejected.push(target.id);
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
try {
|
|
607
|
-
await rejectFn({
|
|
608
|
-
stashDir: opts.stashDir,
|
|
609
|
-
id: target.id,
|
|
610
|
-
reason: target.reason,
|
|
611
|
-
gateDecision: { outcome: "auto-rejected", reason: "empty-diff", gate: gateLabel },
|
|
612
|
-
});
|
|
613
|
-
result.rejected.push(target.id);
|
|
614
|
-
}
|
|
615
|
-
catch (err) {
|
|
616
|
-
const message = pushDrainFailure(result, target.id, err, "reject-error");
|
|
617
|
-
warn(`[triage] reject failed for ${target.id}: ${message}`);
|
|
618
|
-
}
|
|
325
|
+
if (opts.judgment && result.deferred.length > 0) {
|
|
326
|
+
// Symbolic credentials are checked before any gate, reject or promote.
|
|
327
|
+
const prepared = resolveExecution({
|
|
328
|
+
content: "Validate the selected proposal judgment runner before mutation.",
|
|
329
|
+
runner: opts.judgment,
|
|
330
|
+
});
|
|
331
|
+
assertRunnerCredentials(buildExecution(prepared.request, prepared.runner).runner);
|
|
332
|
+
}
|
|
333
|
+
for (const id of empties) {
|
|
334
|
+
const failure = await rejectProposal(opts, id, "empty diff", "empty-diff", rejectFn);
|
|
335
|
+
if (failure === undefined) {
|
|
336
|
+
result.rejected.push(id);
|
|
619
337
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (result.skippedByCap.length > 0) {
|
|
624
|
-
info(`[triage] accept ceiling reached: ${withinCap.length} promoted, ${result.skippedByCap.length} skipped by cap (maxAccepts=${opts.maxAccepts})`);
|
|
338
|
+
else {
|
|
339
|
+
result.failed.push({ id, reason: categorizeDrainFailure(failure, "reject-error"), detail: failure });
|
|
340
|
+
warn(`[triage] reject failed for ${id}: ${failure}`);
|
|
625
341
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
342
|
+
}
|
|
343
|
+
const cap = Math.max(0, opts.maxAccepts);
|
|
344
|
+
const withinCap = accepts.slice(0, cap);
|
|
345
|
+
result.skippedByCap = accepts.slice(cap).map((a) => a.id);
|
|
346
|
+
if (result.skippedByCap.length > 0) {
|
|
347
|
+
info(`[triage] accept ceiling reached: ${withinCap.length} promoted, ${result.skippedByCap.length} skipped by cap (maxAccepts=${opts.maxAccepts})`);
|
|
348
|
+
}
|
|
349
|
+
let promotedHere = 0;
|
|
350
|
+
if (opts.applyMode === "promote") {
|
|
351
|
+
if (!opts.dryRun)
|
|
631
352
|
info(`[triage] auto-promote active: ${withinCap.length} accepts allowed this run`);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
...(opts.config ? { config: opts.config } : {}),
|
|
639
|
-
gateDecision: {
|
|
640
|
-
outcome: "auto-accepted",
|
|
641
|
-
reason: acceptGateReasons.get(id) ?? "policy-accept",
|
|
642
|
-
gate: gateLabel,
|
|
643
|
-
},
|
|
644
|
-
});
|
|
645
|
-
result.promoted.push(id);
|
|
646
|
-
deterministicPromoted += 1;
|
|
647
|
-
}
|
|
648
|
-
catch (err) {
|
|
649
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
650
|
-
if (categorizeDrainFailure(message, "promote-error") === STALE_TARGET_GATE_REASON &&
|
|
651
|
-
(await autoRejectStaleTarget(opts.stashDir, gateLabel, id, message, rejectFn))) {
|
|
652
|
-
result.rejected.push(id);
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
pushDrainFailure(result, id, err, "promote-error");
|
|
656
|
-
warn(`[triage] promote failed for ${id}: ${message}`);
|
|
657
|
-
}
|
|
353
|
+
const byId = new Map(pending.map((proposal) => [proposal.id, proposal]));
|
|
354
|
+
for (const { id, reason } of withinCap) {
|
|
355
|
+
const outcome = await acceptProposal(opts, byId.get(id), id, reason, promoteFn, rejectFn);
|
|
356
|
+
if (outcome === "promoted") {
|
|
357
|
+
result.promoted.push(id);
|
|
358
|
+
promotedHere += 1;
|
|
658
359
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
// Exercise the same stamped candidate, lint, and stale-target boundary as
|
|
662
|
-
// real promotion so a dry-run's predicted promotions match what a real
|
|
663
|
-
// run would do. Tests that omit config retain the classification-only seam.
|
|
664
|
-
const byId = new Map(pending.map((proposal) => [proposal.id, proposal]));
|
|
665
|
-
for (const id of withinCap) {
|
|
666
|
-
try {
|
|
667
|
-
if (opts.config) {
|
|
668
|
-
const proposal = byId.get(id);
|
|
669
|
-
if (!proposal)
|
|
670
|
-
throw new Error(`Proposal ${id} disappeared during drain preflight.`);
|
|
671
|
-
const preflight = preflightProposalPromotion(opts.config, proposal, {
|
|
672
|
-
...(opts.target ? { target: opts.target } : {}),
|
|
673
|
-
gateDecision: {
|
|
674
|
-
outcome: "auto-accepted",
|
|
675
|
-
reason: acceptGateReasons.get(id) ?? "policy-accept",
|
|
676
|
-
gate: gateLabel,
|
|
677
|
-
},
|
|
678
|
-
});
|
|
679
|
-
assertProposalTargetFresh(proposal, preflight.assetPath);
|
|
680
|
-
}
|
|
681
|
-
result.promoted.push(id);
|
|
682
|
-
deterministicPromoted += 1;
|
|
683
|
-
}
|
|
684
|
-
catch (err) {
|
|
685
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
686
|
-
if (categorizeDrainFailure(message, "promote-error") === STALE_TARGET_GATE_REASON) {
|
|
687
|
-
result.rejected.push(id);
|
|
688
|
-
continue;
|
|
689
|
-
}
|
|
690
|
-
pushDrainFailure(result, id, err, "promote-error");
|
|
691
|
-
warn(`[triage] preflight failed for ${id}: ${message}`);
|
|
692
|
-
}
|
|
360
|
+
else if (outcome === "rejected") {
|
|
361
|
+
result.rejected.push(id);
|
|
693
362
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
// Only runs when a RunnerSpec is configured. The runner returns a verdict; the
|
|
702
|
-
// ENGINE performs the resulting accept (respecting applyMode) / reject write.
|
|
703
|
-
if (opts.judgment && result.deferred.length > 0) {
|
|
704
|
-
if (!dispatchLease)
|
|
705
|
-
throw new TypeError("proposal judgment work requires an operation dispatch lease");
|
|
706
|
-
const tier = await runJudgmentTier({
|
|
707
|
-
stashDir: opts.stashDir,
|
|
708
|
-
applyMode: opts.applyMode,
|
|
709
|
-
dryRun: opts.dryRun,
|
|
710
|
-
runner: opts.judgment,
|
|
711
|
-
lease: dispatchLease,
|
|
712
|
-
deferred: result.deferred,
|
|
713
|
-
pending,
|
|
714
|
-
promoteFn,
|
|
715
|
-
rejectFn,
|
|
716
|
-
seams: judgmentSeams,
|
|
717
|
-
...(opts.target ? { target: opts.target } : {}),
|
|
718
|
-
...(opts.config ? { config: opts.config } : {}),
|
|
719
|
-
remainingAcceptBudget,
|
|
720
|
-
gateLabel,
|
|
721
|
-
});
|
|
722
|
-
result.promoted.push(...tier.promoted);
|
|
723
|
-
result.rejected.push(...tier.rejected);
|
|
724
|
-
result.staged.push(...tier.staged);
|
|
725
|
-
if (tier.notices.length > 0)
|
|
726
|
-
result.notices = tier.notices;
|
|
727
|
-
// Judgment-tier accepts dropped by the shared accept cap surface under
|
|
728
|
-
// skippedByCap, same as deterministic cap drops.
|
|
729
|
-
result.skippedByCap.push(...tier.skippedByCap);
|
|
730
|
-
if (tier.skippedByCap.length > 0) {
|
|
731
|
-
info(`[triage] accept ceiling reached in judgment tier: ${tier.skippedByCap.length} judged-accept items skipped by cap (maxAccepts=${opts.maxAccepts})`);
|
|
732
|
-
}
|
|
733
|
-
// Replace the deferred list with only the items the judgment tier could NOT
|
|
734
|
-
// resolve (verdict "defer", parse failure, or runner error). Staged
|
|
735
|
-
// queue-mode accepts are RESOLVED and tracked in result.staged instead.
|
|
736
|
-
result.deferred = tier.stillDeferred;
|
|
737
|
-
}
|
|
738
|
-
else if (result.deferred.length > 0) {
|
|
739
|
-
// #577: no judgment runner configured — items deferred *because they need a
|
|
740
|
-
// judge* (mid-band / possible-dup, no threshold reason) stay pending solely
|
|
741
|
-
// for lack of one. Re-stamp those as `no-judge-configured` so the operator
|
|
742
|
-
// sees a per-proposal reason instead of inferring it from the run-level
|
|
743
|
-
// triage_deferred aggregate. Band-deferred items keep their specific reason
|
|
744
|
-
// (e.g. `max-diff-lines`), which is more actionable than "no judge".
|
|
745
|
-
for (const item of result.deferred) {
|
|
746
|
-
if (needsJudge.has(item.id)) {
|
|
747
|
-
stampGateDecision(opts, item.id, { outcome: "deferred", reason: "no-judge-configured", gate: gateLabel });
|
|
748
|
-
}
|
|
363
|
+
else {
|
|
364
|
+
result.failed.push({
|
|
365
|
+
id,
|
|
366
|
+
reason: categorizeDrainFailure(outcome.message, "promote-error"),
|
|
367
|
+
detail: outcome.message,
|
|
368
|
+
});
|
|
369
|
+
warn(`[triage] ${opts.dryRun ? "preflight" : "promote"} failed for ${id}: ${outcome.message}`);
|
|
749
370
|
}
|
|
750
371
|
}
|
|
751
|
-
emitDrainEvents(opts, result);
|
|
752
|
-
return result;
|
|
753
372
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
disposeLoweredExecutionDispatchLease(dispatchLease);
|
|
373
|
+
if (opts.judgment && result.deferred.length > 0) {
|
|
374
|
+
await runJudgmentTier({ ...opts, judgment: opts.judgment }, result, pending, cap - promotedHere, promoteFn, rejectFn, judgmentSeams);
|
|
757
375
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
catch (err) {
|
|
771
|
-
warn(`[triage] failed to record gate decision for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
376
|
+
// #577: whatever stays undecided is left for review (`review_needed` in the ledger).
|
|
377
|
+
if (!opts.dryRun) {
|
|
378
|
+
const reviewReason = opts.judgment ? "judgment-deferred" : "no-judge-configured";
|
|
379
|
+
for (const item of result.deferred) {
|
|
380
|
+
try {
|
|
381
|
+
recordGateDecision(opts.stashDir, item.id, { outcome: "deferred", reason: reviewReason, gate: DRAIN_GATE });
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
warn(`[triage] failed to record gate decision for ${item.id}: ${errMessage(err)}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
772
387
|
}
|
|
388
|
+
emitDrainEvents(opts, result);
|
|
389
|
+
return result;
|
|
773
390
|
}
|
|
774
|
-
// ---------------------------------------------------------------------------
|
|
775
|
-
// Events
|
|
776
|
-
// ---------------------------------------------------------------------------
|
|
777
391
|
function emitDrainEvents(opts, result) {
|
|
778
392
|
const deferredByReason = {};
|
|
779
|
-
for (const d of result.deferred)
|
|
393
|
+
for (const d of result.deferred)
|
|
780
394
|
deferredByReason[d.reason] = (deferredByReason[d.reason] ?? 0) + 1;
|
|
781
|
-
}
|
|
782
395
|
appendEvent({
|
|
783
396
|
eventType: "triage_drained",
|
|
784
397
|
metadata: {
|
|
@@ -787,17 +400,11 @@ function emitDrainEvents(opts, result) {
|
|
|
787
400
|
deferredByReason,
|
|
788
401
|
skippedByCap: result.skippedByCap.length,
|
|
789
402
|
...(result.staged.length > 0 ? { staged: result.staged.length } : {}),
|
|
790
|
-
policy: opts.policy.name,
|
|
791
403
|
applyMode: opts.applyMode,
|
|
792
404
|
...(opts.dryRun ? { dryRun: true } : {}),
|
|
793
405
|
},
|
|
794
406
|
}, opts.eventsCtx ?? {});
|
|
795
|
-
//
|
|
796
|
-
// tier so a backlog of deferred items never silently looks like full success.
|
|
797
|
-
// This fires when no runner is configured OR the judgment tier ran but could
|
|
798
|
-
// not resolve every item (verdict "defer", parse failure, or a runner error).
|
|
799
|
-
// Queue-mode staged accepts are RESOLVED (the judge decided) and live in
|
|
800
|
-
// result.staged, so they are deliberately excluded from this "unresolved" count.
|
|
407
|
+
// Undecided items must never look like full success. Staged accepts were decided.
|
|
801
408
|
if (result.deferred.length > 0) {
|
|
802
409
|
appendEvent({
|
|
803
410
|
eventType: "triage_deferred",
|