karajan-code 4.70.4 → 5.0.1
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/README.md +2 -2
- package/package.json +6 -14
- package/packages/hu-board/package.json +1 -1
- package/packages/hu-board/public/app.js +4 -18
- package/packages/hu-board/public/index.html +5 -2
- package/packages/hu-board/public/styles.css +26 -0
- package/packages/hu-board/public/utils/api.js +0 -74
- package/packages/hu-board/public/utils/board-tour.js +62 -0
- package/packages/hu-board/public/utils/board-view.js +49 -144
- package/packages/hu-board/public/utils/command-launcher.js +8 -70
- package/packages/hu-board/public/utils/formatters.js +10 -0
- package/packages/hu-board/public/utils/governance-view.js +13 -1
- package/packages/hu-board/public/utils/hu-actions.js +17 -68
- package/packages/hu-board/public/utils/init-listeners.js +7 -8
- package/packages/hu-board/public/utils/log-panel.js +3 -16
- package/packages/hu-board/public/utils/maggle-mode.js +2 -12
- package/packages/hu-board/public/utils/modals.js +7 -0
- package/packages/hu-board/public/utils/project-picker-view.js +1 -1
- package/packages/hu-board/public/utils/server-push.js +3 -22
- package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
- package/packages/hu-board/public/utils/story-detail-view.js +5 -19
- package/packages/hu-board/src/cleanup-zombies.js +0 -1
- package/packages/hu-board/src/command-runner.js +36 -25
- package/packages/hu-board/src/db-connection.js +282 -0
- package/packages/hu-board/src/db-paths.js +113 -0
- package/packages/hu-board/src/db-queries.js +188 -0
- package/packages/hu-board/src/db-tombstones.js +51 -0
- package/packages/hu-board/src/db.js +28 -619
- package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
- package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
- package/packages/hu-board/src/plan-mutations.js +3 -153
- package/packages/hu-board/src/plans-service.js +55 -0
- package/packages/hu-board/src/routes/api/commands.js +122 -0
- package/packages/hu-board/src/routes/api/plans.js +55 -0
- package/packages/hu-board/src/routes/api/projects.js +267 -0
- package/packages/hu-board/src/routes/api/prompts.js +135 -0
- package/packages/hu-board/src/routes/api/sessions.js +95 -0
- package/packages/hu-board/src/routes/api/stories.js +183 -0
- package/packages/hu-board/src/routes/api/system.js +241 -0
- package/packages/hu-board/src/routes/api/tombstones.js +38 -0
- package/packages/hu-board/src/routes/api.js +24 -1371
- package/packages/hu-board/src/routes/governance.js +23 -1
- package/packages/hu-board/src/routes/lanes.js +40 -0
- package/packages/hu-board/src/routes/rag.js +4 -3
- package/packages/hu-board/src/server.js +20 -34
- package/packages/hu-board/src/sync-files.js +318 -0
- package/packages/hu-board/src/sync-project.js +99 -0
- package/packages/hu-board/src/sync.js +22 -566
- package/packages/hu-board/src/zombie-reaper.js +1 -1
- package/scripts/dual-publish.mjs +19 -3
- package/scripts/postinstall.js +15 -2
- package/scripts/verify-pack-mcp.mjs +11 -7
- package/scripts/verify-pack.mjs +4 -4
- package/src/audit/audit-history.js +3 -2
- package/src/audit/dead-exports.js +68 -59
- package/src/audit/deterministic-summary.js +33 -13
- package/src/audit/npm-signatures.js +41 -0
- package/src/audit/sbom.js +35 -0
- package/src/checks/agent-token.js +112 -0
- package/src/checks/dir-setup.js +34 -1
- package/src/checks/native-build.js +21 -39
- package/src/checks/runner.js +128 -142
- package/src/cli/advanced-commands.js +6 -6
- package/src/cli/register-meta.js +42 -82
- package/src/cli/register-pipeline.js +8 -143
- package/src/cli/register-plan.js +9 -100
- package/src/cli/register-roles-skills.js +1 -1
- package/src/cli/retired-commands.js +49 -0
- package/src/cli.js +18 -5
- package/src/commands/agent-run.js +40 -26
- package/src/commands/audit.js +127 -124
- package/src/commands/board.js +5 -9
- package/src/commands/check.js +29 -16
- package/src/commands/doctor.js +2 -0
- package/src/commands/env.js +176 -150
- package/src/commands/go.js +71 -53
- package/src/commands/harden.js +90 -81
- package/src/commands/hu-start.js +52 -0
- package/src/commands/hu.js +288 -221
- package/src/commands/identity.js +111 -78
- package/src/commands/init-atril.js +47 -0
- package/src/commands/init.js +11 -40
- package/src/commands/install-tools.js +70 -85
- package/src/commands/main-guard.js +78 -0
- package/src/commands/policy.js +226 -184
- package/src/commands/qmd.js +27 -21
- package/src/commands/rag.js +121 -55
- package/src/commands/review-gate.js +421 -341
- package/src/commands/skills.js +4 -1
- package/src/commands/start.js +4 -5
- package/src/commands/steward.js +96 -47
- package/src/commands/tournament.js +46 -37
- package/src/commands/worktree.js +107 -26
- package/src/config/defaults.js +1 -0
- package/src/config/loader.js +34 -17
- package/src/config/role-resolver.js +1 -0
- package/src/config/schema.js +1 -0
- package/src/config/secrets.js +36 -0
- package/src/config.js +1 -0
- package/src/environment/adr.js +16 -2
- package/src/git/karajan-label.js +20 -8
- package/src/guards/main-guard.js +54 -0
- package/src/harden/hook-templates.js +16 -1
- package/src/harden/sentinel/posttooluse.mjs +18 -6
- package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
- package/src/harden/sentinel/sentinel-lib.mjs +129 -26
- package/src/harden/sentinel/sentinel-shell.mjs +5 -16
- package/src/harden/sentinel/sessionstart.mjs +3 -2
- package/src/harden/sentinel/stop.mjs +5 -4
- package/src/harden/workflow-templates.js +1 -1
- package/src/hu/board-lookup.js +29 -0
- package/src/hu/gh-identity.js +23 -0
- package/src/hu/github-board.js +4 -2
- package/src/hu/lane-prune.js +83 -13
- package/src/hu/lane-setup.js +65 -0
- package/src/hu/lanes.js +66 -0
- package/src/hu/team-board-mirror.js +3 -2
- package/src/infrastructure/command-runner.js +1 -1
- package/src/infrastructure/file-system-service.js +1 -1
- package/src/mcp/handlers/direct-handlers.js +2 -292
- package/src/mcp/handlers/hu-handlers.js +1 -35
- package/src/mcp/handlers/management-handlers.js +2 -42
- package/src/mcp/handlers/rag-handler.js +9 -1
- package/src/mcp/server-handlers.js +16 -22
- package/src/mcp/tools.js +1 -196
- package/src/plan/lane-plans.js +54 -0
- package/src/plan/plan-store.js +7 -4
- package/src/policy/add.js +26 -12
- package/src/policy/adr-number-check.js +31 -0
- package/src/prompts/audit.js +299 -339
- package/src/prompts/card-context.js +10 -5
- package/src/prompts/reviewer.js +5 -2
- package/src/rag/auto-update.js +71 -44
- package/src/rag/bootstrap-index.js +46 -54
- package/src/rag/indexer.js +9 -4
- package/src/rag/migrate.js +1 -1
- package/src/rag/onnx-fallback.js +2 -2
- package/src/rag/pid-lease.js +181 -0
- package/src/rag/project-store.js +2 -12
- package/src/review/card-first.js +57 -46
- package/src/review/gate-gitignore.js +23 -4
- package/src/review/gate-hook.js +12 -4
- package/src/review/one-shot-review.js +45 -35
- package/src/review/publish-pr.js +14 -2
- package/src/review/repo-docs.js +73 -0
- package/src/review/repo-stack.js +59 -0
- package/src/review/reviewer-fallback.js +19 -4
- package/src/review/seal-commit.js +29 -0
- package/src/review/sonar-pregate.js +59 -44
- package/src/review/third-party-review.js +90 -16
- package/src/review/third-party-size.js +43 -0
- package/src/review/third-party-skills.js +54 -0
- package/src/review/verify-pr.js +1 -24
- package/src/roles/audit-role.js +106 -125
- package/src/skills/atril-cli.js +19 -1
- package/src/skills/atril-skills.js +5 -4
- package/src/skills/skills-search.js +72 -0
- package/src/skills/translate.js +62 -0
- package/src/sonar/scanner.js +116 -101
- package/src/steward/invariants.js +5 -4
- package/src/steward/phantom-sweep.js +66 -0
- package/src/tournament/judge.js +60 -53
- package/src/utils/gh-account.js +37 -0
- package/src/utils/git.js +0 -13
- package/src/utils/home-migration.js +7 -1
- package/src/utils/main-checkout.js +53 -0
- package/src/utils/qmd-collection.js +3 -1
- package/src/utils/welcome.js +3 -3
- package/templates/{skills → atril/commands}/kj-architect.md +5 -0
- package/templates/{skills → atril/commands}/kj-audit.md +5 -0
- package/templates/{skills → atril/commands}/kj-board.md +5 -0
- package/templates/{skills → atril/commands}/kj-code.md +5 -0
- package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
- package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
- package/templates/{skills → atril/commands}/kj-discover.md +5 -0
- package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
- package/templates/{skills → atril/commands}/kj-review.md +5 -0
- package/templates/{skills → atril/commands}/kj-security.md +5 -0
- package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
- package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
- package/templates/{skills → atril/commands}/kj-test.md +5 -0
- package/packages/hu-board/public/utils/run-launcher.js +0 -169
- package/packages/hu-board/src/run-tracker.js +0 -121
- package/src/activity-log.js +0 -59
- package/src/agents/host-agent.js +0 -38
- package/src/agents/model-registry.js +0 -142
- package/src/audit/plan-adherence.js +0 -224
- package/src/autonomy/arbiter-role.js +0 -71
- package/src/autonomy/arbiter.js +0 -22
- package/src/autonomy/decider.js +0 -18
- package/src/autonomy/decision-resolver.js +0 -58
- package/src/autonomy/outcome-report.js +0 -55
- package/src/autonomy/stage-ask.js +0 -30
- package/src/brain/decision-tracker.js +0 -125
- package/src/brain/decisor.js +0 -243
- package/src/brain/solomon-consult.js +0 -171
- package/src/brain/standby-scheduler.js +0 -8
- package/src/budget/comparison.js +0 -96
- package/src/canvas/parse-md.js +0 -238
- package/src/canvas/schema.js +0 -101
- package/src/canvas/to-flat.js +0 -85
- package/src/canvas/validate.js +0 -163
- package/src/ci/dispatch.js +0 -99
- package/src/ci/index.js +0 -3
- package/src/ci/pr-diff.js +0 -26
- package/src/cli/register-standby.js +0 -26
- package/src/commands/architect.js +0 -111
- package/src/commands/autorun.js +0 -49
- package/src/commands/researcher.js +0 -54
- package/src/commands/resume.js +0 -72
- package/src/commands/review.js +0 -89
- package/src/commands/run.js +0 -185
- package/src/commands/standby.js +0 -117
- package/src/commands/status.js +0 -36
- package/src/commands/triage.js +0 -78
- package/src/commands/undo.js +0 -62
- package/src/domains/domain-loader.js +0 -245
- package/src/domains/domain-registry.js +0 -140
- package/src/domains/domain-synthesizer.js +0 -129
- package/src/git/automation.js +0 -433
- package/src/git/coder-commit-guard.js +0 -40
- package/src/git/hu-automation.js +0 -243
- package/src/git/hu-snapshot.js +0 -10
- package/src/git/symlink-targets.js +0 -57
- package/src/golden/asserter.js +0 -92
- package/src/golden/loader.js +0 -52
- package/src/golden/runner.js +0 -92
- package/src/golden/schema.js +0 -41
- package/src/guards/duplicate-members.js +0 -86
- package/src/guards/intent-guard.js +0 -161
- package/src/guards/perf-guard.js +0 -126
- package/src/guards/policy-guard.js +0 -90
- package/src/guards/policy-resolver.js +0 -85
- package/src/guards/secret-redactor.js +0 -71
- package/src/hu/acceptance-runner.js +0 -138
- package/src/hu/lazy-planner.js +0 -109
- package/src/hu/parallel-executor.js +0 -94
- package/src/hu/routing-metrics.js +0 -17
- package/src/hu/split-validator.js +0 -143
- package/src/hu/splitting-detector.js +0 -158
- package/src/hu/splitting-generator.js +0 -175
- package/src/hu/store.js +0 -394
- package/src/infrastructure/mocks.js +0 -182
- package/src/mcp/handlers/run-handler.js +0 -318
- package/src/mcp/sovereignty-guard.js +0 -113
- package/src/mcp/tool-arg-normalizers.js +0 -16
- package/src/mutate/reviewer-signal.js +0 -76
- package/src/orchestrator/agent-fallback.js +0 -81
- package/src/orchestrator/brain-coordinator.js +0 -186
- package/src/orchestrator/ci-integration.js +0 -139
- package/src/orchestrator/config-init.js +0 -426
- package/src/orchestrator/direct-actions.js +0 -217
- package/src/orchestrator/drivers/error-recovery.js +0 -287
- package/src/orchestrator/drivers/init-context.js +0 -238
- package/src/orchestrator/drivers/iteration-loop.js +0 -382
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
- package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
- package/src/orchestrator/drivers/post-loop.js +0 -404
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
- package/src/orchestrator/drivers/pre-loop.js +0 -524
- package/src/orchestrator/drivers/run-hu-batch.js +0 -468
- package/src/orchestrator/feedback-enrichment.js +0 -140
- package/src/orchestrator/feedback-queue.js +0 -151
- package/src/orchestrator/flow-control.js +0 -170
- package/src/orchestrator/flow-runner.js +0 -365
- package/src/orchestrator/fs-leak-detector.js +0 -228
- package/src/orchestrator/hu-scheduler.js +0 -114
- package/src/orchestrator/hu-sub-pipeline.js +0 -642
- package/src/orchestrator/iteration-gate.js +0 -73
- package/src/orchestrator/iteration-stages.js +0 -9
- package/src/orchestrator/parallel-limiter.js +0 -74
- package/src/orchestrator/pipeline-context.js +0 -72
- package/src/orchestrator/post-loop-stages.js +0 -468
- package/src/orchestrator/pre-loop-stages.js +0 -11
- package/src/orchestrator/preflight-checks.js +0 -487
- package/src/orchestrator/repair/repair-detector.js +0 -117
- package/src/orchestrator/repair/repair-runner.js +0 -142
- package/src/orchestrator/reviewer-fallback.js +0 -45
- package/src/orchestrator/role-output-compressor.js +0 -189
- package/src/orchestrator/session-journal.js +0 -308
- package/src/orchestrator/solomon-escalation.js +0 -331
- package/src/orchestrator/solomon-rules.js +0 -212
- package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
- package/src/orchestrator/stages/architect-stage.js +0 -172
- package/src/orchestrator/stages/coder-stage.js +0 -506
- package/src/orchestrator/stages/domain-curator-stage.js +0 -82
- package/src/orchestrator/stages/format-stage.js +0 -61
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
- package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
- package/src/orchestrator/stages/perf-stage.js +0 -140
- package/src/orchestrator/stages/planner-stage.js +0 -85
- package/src/orchestrator/stages/rag-context-stage.js +0 -80
- package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
- package/src/orchestrator/stages/research-stage.js +0 -69
- package/src/orchestrator/stages/reviewer-stage.js +0 -431
- package/src/orchestrator/stages/sonar-stage.js +0 -439
- package/src/orchestrator/stages/stage-classes.js +0 -84
- package/src/orchestrator/stages/stage-executor.js +0 -155
- package/src/orchestrator/stages/suite-green-stage.js +0 -62
- package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
- package/src/orchestrator/stages/tool-judge-stage.js +0 -53
- package/src/orchestrator/stages/triage-stage.js +0 -172
- package/src/orchestrator/standby.js +0 -71
- package/src/orchestrator/verification-gate.js +0 -183
- package/src/orchestrator.js +0 -22
- package/src/plan/adr-generator.js +0 -170
- package/src/plan/adr-loader.js +0 -60
- package/src/plan/plan-executor.js +0 -100
- package/src/plan/plan-validation.js +0 -4
- package/src/prompts/acceptance-coverage.js +0 -32
- package/src/prompts/arbiter.js +0 -38
- package/src/prompts/architect.js +0 -103
- package/src/prompts/hu-reviewer.js +0 -244
- package/src/prompts/tool-correctness-judge.js +0 -31
- package/src/prompts/triage.js +0 -57
- package/src/rag/embedder.js +0 -2
- package/src/rag/embedders/_cloud-base.js +0 -2
- package/src/rag/embedders/cohere.js +0 -2
- package/src/rag/embedders/mistral.js +0 -2
- package/src/rag/embedders/onnx.js +0 -2
- package/src/rag/embedders/openai.js +0 -2
- package/src/rag/embedders/voyage.js +0 -2
- package/src/rag/rerank.js +0 -4
- package/src/rag/where-parser.js +0 -4
- package/src/repeat-detector.js +0 -88
- package/src/review/diff-generator.js +0 -112
- package/src/review/git-stash-helper.js +0 -104
- package/src/review/schema.js +0 -31
- package/src/review/scope-filter.js +0 -152
- package/src/review/snapshot-diff.js +0 -122
- package/src/review/tdd-discipline.js +0 -90
- package/src/review/tdd-policy.js +0 -69
- package/src/roles/acceptance-coverage-role.js +0 -78
- package/src/roles/architect-role.js +0 -77
- package/src/roles/coder-role.js +0 -113
- package/src/roles/commiter-role.js +0 -113
- package/src/roles/domain-curator-role.js +0 -134
- package/src/roles/hu-reviewer-role.js +0 -64
- package/src/roles/impeccable-role.js +0 -85
- package/src/roles/index.js +0 -18
- package/src/roles/karajan-brain-role.js +0 -134
- package/src/roles/perf-role.js +0 -122
- package/src/roles/planner-role.js +0 -151
- package/src/roles/refactorer-role.js +0 -30
- package/src/roles/repairer-role.js +0 -181
- package/src/roles/researcher-role.js +0 -70
- package/src/roles/reviewer-role.js +0 -115
- package/src/roles/security-role.js +0 -73
- package/src/roles/solomon-role.js +0 -271
- package/src/roles/sonar-role.js +0 -72
- package/src/roles/tester-role.js +0 -235
- package/src/roles/tool-correctness-judge-role.js +0 -78
- package/src/roles/triage-role.js +0 -99
- package/src/session/journal/decisions-writer.js +0 -200
- package/src/session/journal/iteration-logger.js +0 -194
- package/src/session/journal/summary-writer.js +0 -320
- package/src/session/journal/tree-writer.js +0 -164
- package/src/session/journal/write-all.js +0 -89
- package/src/session-cleanup.js +0 -116
- package/src/skills/semantic-detector.js +0 -167
- package/src/skills/skill-detector.js +0 -385
- package/src/sonar/cloud-scanner.js +0 -78
- package/src/tracker-bootstrap.js +0 -44
- package/src/types/index.js +0 -38
- package/src/types/policy.js +0 -43
- package/src/types/solomon.js +0 -87
- package/src/utils/async-lock.js +0 -31
- package/src/utils/budget.js +0 -229
- package/src/utils/cwd-confirm.js +0 -39
- package/src/utils/display/event-handlers.js +0 -403
- package/src/utils/display/formatters.js +0 -132
- package/src/utils/display/header.js +0 -47
- package/src/utils/display/session-summary.js +0 -118
- package/src/utils/display/solomon.js +0 -42
- package/src/utils/pricing.js +0 -29
- package/src/utils/quota-simulator.js +0 -56
- package/src/utils/role-assigner.js +0 -137
- package/src/utils/rtk-wrapper.js +0 -87
- package/src/utils/run-registry.js +0 -9
- package/src/utils/status-dashboard.js +0 -254
- package/src/utils/tool-call-extractor.js +0 -56
- package/src/webperf/devtools-detect.js +0 -112
- package/src/webperf/webperf-role-integration.js +0 -66
- package/templates/skills/kj-run.md +0 -73
|
@@ -1,642 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HU Sub-Pipeline — executes each certified HU as an independent sub-pipeline
|
|
3
|
-
* with per-HU state tracking, topological ordering, and dependency-aware blocking.
|
|
4
|
-
*/
|
|
5
|
-
import { assertNoCycle, topologicalSort } from "../hu/graph.js";
|
|
6
|
-
import { updateStoryStatus, loadHuBatch, saveHuBatch, HU_STATUS } from "../hu/store.js";
|
|
7
|
-
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
8
|
-
import { refineHuWithContext } from "../hu/lazy-planner.js";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { acquireSlot, releaseSlot } from "karajan-core/slot-registry";
|
|
11
|
-
import { getKarajanHome } from "karajan-core/paths";
|
|
12
|
-
import { findParallelGroups, createWorktree, mergeWorktree, removeWorktree } from "../hu/parallel-executor.js";
|
|
13
|
-
import { bootstrapWorktree } from "../hu/worktree-bootstrap.js";
|
|
14
|
-
import { partitionConflictFree } from "./hu-scheduler.js";
|
|
15
|
-
import { createParallelLimiter, planBudgetUsd } from "./parallel-limiter.js";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Determine whether the HU reviewer result needs the sub-pipeline path.
|
|
19
|
-
*
|
|
20
|
-
* Pre-fix this returned true only when there were > 1 certified stories.
|
|
21
|
-
* That broke single-HU runs (`kj run --plan <id> --hu <huId>`, the path
|
|
22
|
-
* the board's per-card ▶ button drives): with exactly one HU certified
|
|
23
|
-
* the flow fell through to the standard single-task pipeline, which
|
|
24
|
-
* does NOT call updateStoryStatus / saveHuBatch / emit hu:* events.
|
|
25
|
-
* Result: the HU stayed forever in `coding` even after the run finished
|
|
26
|
-
* because nobody persisted "done"/"failed" anywhere — board kanban
|
|
27
|
-
* showed "1 running…" indefinitely, plan JSON kept the stale status.
|
|
28
|
-
*
|
|
29
|
-
* The sub-pipeline already handles a 1-HU batch as a degenerate case
|
|
30
|
-
* (no dependency graph to resolve, no parallel batching) — so the fix
|
|
31
|
-
* is to take that path for any non-empty batch, not just >1.
|
|
32
|
-
*
|
|
33
|
-
* @param {object|null} huReviewerResult - stageResults.huReviewer
|
|
34
|
-
* @returns {boolean}
|
|
35
|
-
*/
|
|
36
|
-
export function needsSubPipeline(huReviewerResult) {
|
|
37
|
-
if (!huReviewerResult?.ok) return false;
|
|
38
|
-
const certified = (huReviewerResult.stories || []).filter(
|
|
39
|
-
s => s.status === "certified"
|
|
40
|
-
);
|
|
41
|
-
return certified.length >= 1;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Mark all stories that depend (directly or transitively) on a failed story as "blocked".
|
|
46
|
-
* @param {object} batch - The HU batch object.
|
|
47
|
-
* @param {string} failedId - The ID of the failed story.
|
|
48
|
-
* @returns {string[]} IDs of newly blocked stories.
|
|
49
|
-
*/
|
|
50
|
-
export function blockDependents(batch, failedId) {
|
|
51
|
-
const blocked = [];
|
|
52
|
-
const failedSet = new Set([failedId]);
|
|
53
|
-
// Iterate until no new blocked stories are found (handles transitive deps)
|
|
54
|
-
let changed = true;
|
|
55
|
-
while (changed) {
|
|
56
|
-
changed = false;
|
|
57
|
-
for (const story of batch.stories) {
|
|
58
|
-
if (failedSet.has(story.id)) continue;
|
|
59
|
-
if (story.status === HU_STATUS.BLOCKED) continue;
|
|
60
|
-
const deps = story.blocked_by || [];
|
|
61
|
-
if (deps.some(dep => failedSet.has(dep))) {
|
|
62
|
-
updateStoryStatus(batch, story.id, HU_STATUS.BLOCKED);
|
|
63
|
-
failedSet.add(story.id);
|
|
64
|
-
blocked.push(story.id);
|
|
65
|
-
changed = true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return blocked;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Build task description for the coder from a certified HU story.
|
|
74
|
-
* @param {object} story - A story object from the batch.
|
|
75
|
-
* @returns {string}
|
|
76
|
-
*/
|
|
77
|
-
function buildHuTask(story) {
|
|
78
|
-
if (story.certified?.text) return story.certified.text;
|
|
79
|
-
if (story.original?.text) return story.original.text;
|
|
80
|
-
return `Implement HU ${story.id}`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Sum the cost_usd of a slice of BudgetTracker entries. Returns null
|
|
85
|
-
* (not 0) when the slice is empty or contains no usable cost data —
|
|
86
|
-
* the board uses null as "unmeasured" and renders no badge, while 0
|
|
87
|
-
* means "really free run". Mixing the two would silently hide free
|
|
88
|
-
* runs and falsely show "$0.00" for runs with no telemetry.
|
|
89
|
-
*
|
|
90
|
-
* Cost D (KJC-TSK-0515): the orchestrator calls this once per HU at
|
|
91
|
-
* runSingleHu exit, passing the slice of entries recorded during that
|
|
92
|
-
* HU (snapshotted via the entries.length cursor before the iteration
|
|
93
|
-
* starts). The resulting number flows through buildHuOutcome →
|
|
94
|
-
* notifyOutcome → setStoryCost in inject-loaded-plan.js.
|
|
95
|
-
*
|
|
96
|
-
* @param {Array<{cost_usd?: number}>} entries
|
|
97
|
-
* @returns {number|null}
|
|
98
|
-
*/
|
|
99
|
-
function sumCostUsd(entries) {
|
|
100
|
-
if (!Array.isArray(entries) || entries.length === 0) return null;
|
|
101
|
-
let total = 0;
|
|
102
|
-
let hasAny = false;
|
|
103
|
-
for (const e of entries) {
|
|
104
|
-
const n = Number(e?.cost_usd);
|
|
105
|
-
if (Number.isFinite(n)) {
|
|
106
|
-
total += n;
|
|
107
|
-
hasAny = true;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (!hasAny) return null;
|
|
111
|
-
return Math.round(total * 1e6) / 1e6;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Sum an integer token-counter field across a slice of BudgetTracker
|
|
116
|
-
* entries (Φ0-G, KJC-TSK-0525). Same null-vs-0 semantics as sumCostUsd:
|
|
117
|
-
* returns null when nothing measurable was recorded so the board can
|
|
118
|
-
* distinguish "unmeasured" from "really zero".
|
|
119
|
-
*
|
|
120
|
-
* @param {Array<object>} entries
|
|
121
|
-
* @param {string} field — entry key to sum, e.g. "cached_tokens"
|
|
122
|
-
* @returns {number|null}
|
|
123
|
-
*/
|
|
124
|
-
function sumTokenField(entries, field) {
|
|
125
|
-
if (!Array.isArray(entries) || entries.length === 0) return null;
|
|
126
|
-
let total = 0;
|
|
127
|
-
let hasAny = false;
|
|
128
|
-
for (const e of entries) {
|
|
129
|
-
const n = Number(e?.[field]);
|
|
130
|
-
if (Number.isFinite(n) && n >= 0) {
|
|
131
|
-
total += n;
|
|
132
|
-
hasAny = true;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (!hasAny) return null;
|
|
136
|
-
return Math.round(total);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Build the per-HU outcome blob from whatever metadata the iteration
|
|
141
|
-
* loop returned. Tolerant by design — most fields are optional and we
|
|
142
|
-
* default to safe values so the board can render something useful
|
|
143
|
-
* even on weird failure modes (HU crashed before producing summary,
|
|
144
|
-
* runIterationFn missing the commits array, etc.).
|
|
145
|
-
*
|
|
146
|
-
* Plain-Spanish summary lives at the bottom of the priority order so
|
|
147
|
-
* we surface what the run actually said before falling back to
|
|
148
|
-
* generic phrases.
|
|
149
|
-
*
|
|
150
|
-
* @param {object} args
|
|
151
|
-
* @param {object} args.story
|
|
152
|
-
* @param {object} args.iterResult — whatever runIterationFn returned
|
|
153
|
-
* @param {string} args.status — HU_STATUS.* terminal value
|
|
154
|
-
* @param {number} args.startedAt — Date.now() when runSingleHu started
|
|
155
|
-
* @param {Array} [args.huBudgetEntries] — slice of BudgetTracker
|
|
156
|
-
* entries recorded during this HU. Cost D (KJC-TSK-0515).
|
|
157
|
-
* @returns {object}
|
|
158
|
-
*/
|
|
159
|
-
function buildHuOutcome({ story, iterResult, status, startedAt, huBudgetEntries }) {
|
|
160
|
-
const r = iterResult || {};
|
|
161
|
-
const git = r.git || {};
|
|
162
|
-
// Iterations are tracked in the standard pipeline as
|
|
163
|
-
// `result.iterations`, in the tests-first pipeline as the loop
|
|
164
|
-
// counter — fall back to whatever we can find.
|
|
165
|
-
const iterations = r.iterations ?? r.iteration ?? r.attempts ?? null;
|
|
166
|
-
const commits = Array.isArray(git.commits)
|
|
167
|
-
? git.commits.map((c) => (typeof c === "string" ? c : c.hash || c.sha || "")).filter(Boolean)
|
|
168
|
-
: (Array.isArray(r.commits) ? r.commits : []);
|
|
169
|
-
const blockers = [];
|
|
170
|
-
if (r.error) blockers.push(String(r.error));
|
|
171
|
-
if (r.reason && r.reason !== "approved") blockers.push(String(r.reason));
|
|
172
|
-
if (Array.isArray(r.deferredIssues)) {
|
|
173
|
-
for (const d of r.deferredIssues) blockers.push(typeof d === "string" ? d : d.message || JSON.stringify(d));
|
|
174
|
-
}
|
|
175
|
-
// KJC-BUG-0248: a push or PR that failed is a blocker the board shows.
|
|
176
|
-
for (const e of git.errors ?? []) blockers.push(`${e.step} failed: ${e.message}. ${e.remedy}`);
|
|
177
|
-
// Plain-Spanish, one-or-two sentences.
|
|
178
|
-
let summary;
|
|
179
|
-
if (r.review?.summary) summary = String(r.review.summary);
|
|
180
|
-
else if (status === "done") summary = `HU completada${commits.length ? ` con ${commits.length} commit(s)` : ""}.`;
|
|
181
|
-
else if (status === "failed") summary = `HU falló${blockers.length ? `: ${blockers[0]}` : ""}.`;
|
|
182
|
-
else summary = `HU ${status}.`;
|
|
183
|
-
return {
|
|
184
|
-
status,
|
|
185
|
-
iterations: typeof iterations === "number" ? iterations : null,
|
|
186
|
-
duration_ms: typeof startedAt === "number" ? Date.now() - startedAt : null,
|
|
187
|
-
branch: r.branch || git.branch || null,
|
|
188
|
-
commits,
|
|
189
|
-
pr_url: r.pr_url || r.prUrl || git.pr_url || null,
|
|
190
|
-
blockers,
|
|
191
|
-
summary,
|
|
192
|
-
cost_usd: sumCostUsd(huBudgetEntries),
|
|
193
|
-
// Φ0-G (KJC-TSK-0525): cached/input tokens drive the "🎯 N% cached"
|
|
194
|
-
// board badge. Null when nothing measurable was recorded so the UI
|
|
195
|
-
// can hide the chip on cold runs or providers without cache.
|
|
196
|
-
cached_tokens: sumTokenField(huBudgetEntries, 'cached_tokens'),
|
|
197
|
-
tokens_in: sumTokenField(huBudgetEntries, 'tokens_in'),
|
|
198
|
-
// KJC-TSK-0702 (MR-E): the baseline model routing is measured against.
|
|
199
|
-
tokens_out: sumTokenField(huBudgetEntries, 'tokens_out'),
|
|
200
|
-
...routingBaseline(huBudgetEntries, status),
|
|
201
|
-
// KJC-TSK-1071 (MR-D3): the routing decision applyCoderRoute left on the HU.
|
|
202
|
-
coder_routing: story?.coder_routing ?? null,
|
|
203
|
-
finishedAt: new Date().toISOString(),
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* KJC-TSK-0702 (MR-E): which model the coder used and how many times the
|
|
209
|
-
* reviewer sent the HU back: its rounds, minus the approval that closed a done HU.
|
|
210
|
-
* Null when nothing was recorded, as "unmeasured".
|
|
211
|
-
*/
|
|
212
|
-
function routingBaseline(entries, status) {
|
|
213
|
-
const list = Array.isArray(entries) ? entries : [];
|
|
214
|
-
const coder = list.findLast((e) => e?.role === "coder");
|
|
215
|
-
const reviewerRounds = list.filter((e) => e?.role === "reviewer").length;
|
|
216
|
-
const approvalRound = status === "done" ? 1 : 0;
|
|
217
|
-
return {
|
|
218
|
-
coder: coder ? { provider: coder.provider ?? null, model: coder.model ?? null } : null,
|
|
219
|
-
reviewer_rejections: reviewerRounds === 0 ? null : reviewerRounds - approvalRound,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Execute a single HU through the coder→sonar→reviewer sub-pipeline.
|
|
225
|
-
* Extracted to support both sequential and parallel execution paths.
|
|
226
|
-
*
|
|
227
|
-
* @param {object} params
|
|
228
|
-
* @returns {Promise<{huId: string, approved: boolean, result?: object, error?: string, blockedDependents?: string[]}>}
|
|
229
|
-
*/
|
|
230
|
-
async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emitter, eventBase, logger, config, results, worktreePath, laneSlot = null, onStatusChange = null, onOutcome = null, budgetTracker = null }) {
|
|
231
|
-
// PR1 (live HU status): every saveHuBatch should also notify the
|
|
232
|
-
// plan JSON so the board reflects state in real time. Defined as a
|
|
233
|
-
// local helper so we don't repeat the try/null-check boilerplate.
|
|
234
|
-
const notifyStatus = async (huId, status) => {
|
|
235
|
-
if (typeof onStatusChange !== "function") return;
|
|
236
|
-
try { await onStatusChange(huId, status); }
|
|
237
|
-
catch (err) { logger?.warn?.(`onStatusChange(${huId}, ${status}) failed: ${err.message}`); }
|
|
238
|
-
};
|
|
239
|
-
// PR3 (per-HU outcome): single notification at the end of the HU
|
|
240
|
-
// with the rich outcome blob. Plan JSON gets it stamped before
|
|
241
|
-
// we return, so the board's chokidar fires once and the card
|
|
242
|
-
// shows its summary indicator.
|
|
243
|
-
const notifyOutcome = async (huId, outcome) => {
|
|
244
|
-
if (typeof onOutcome !== "function") return;
|
|
245
|
-
try { await onOutcome(huId, outcome); }
|
|
246
|
-
catch (err) { logger?.warn?.(`onOutcome(${huId}) failed: ${err.message}`); }
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
// PR3: capture the start time so duration can be reported in the
|
|
250
|
-
// outcome (wall-clock per HU is what the user needs in the board,
|
|
251
|
-
// not internal stage timings).
|
|
252
|
-
const huStartedAt = Date.now();
|
|
253
|
-
// Cost D (KJC-TSK-0515): snapshot the budget cursor so we can slice
|
|
254
|
-
// out only the entries this HU produced. BudgetTracker is session-
|
|
255
|
-
// scoped (it accumulates across all HUs of the run), so without this
|
|
256
|
-
// cursor every HU after the first would inherit the previous ones'
|
|
257
|
-
// cost too. Null-safe — older callers without a tracker still work.
|
|
258
|
-
const huBudgetStartIdx = budgetTracker?.entries?.length ?? 0;
|
|
259
|
-
const sliceHuBudget = () => {
|
|
260
|
-
if (!budgetTracker?.entries) return [];
|
|
261
|
-
return budgetTracker.entries.slice(huBudgetStartIdx);
|
|
262
|
-
};
|
|
263
|
-
const story = batch.stories.find(s => s.id === storyId);
|
|
264
|
-
|
|
265
|
-
// --- Lazy refinement ---
|
|
266
|
-
if (story.needsRefinement && config) {
|
|
267
|
-
const completedHus = results
|
|
268
|
-
.filter(r => r.approved)
|
|
269
|
-
.map(r => {
|
|
270
|
-
const completedStory = batch.stories.find(s => s.id === r.huId);
|
|
271
|
-
return {
|
|
272
|
-
...completedStory,
|
|
273
|
-
resultSummary: r.result?.summary || r.result?.reason || null
|
|
274
|
-
};
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
emitProgress(emitter, makeEvent("hu:refine-start", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
278
|
-
message: `Refining HU ${story.id} with context from ${completedHus.length} completed HU(s)`,
|
|
279
|
-
detail: { huId: story.id, completedCount: completedHus.length }
|
|
280
|
-
}));
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
const refined = await refineHuWithContext(story, completedHus, config, logger);
|
|
284
|
-
Object.assign(story, refined);
|
|
285
|
-
story.needsRefinement = false;
|
|
286
|
-
await saveHuBatch(batchSessionId, batch);
|
|
287
|
-
|
|
288
|
-
emitProgress(emitter, makeEvent("hu:refine-end", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
289
|
-
message: `HU ${story.id} refined successfully`,
|
|
290
|
-
detail: { huId: story.id }
|
|
291
|
-
}));
|
|
292
|
-
} catch (err) {
|
|
293
|
-
logger.warn(`Lazy refinement failed for HU ${story.id}: ${err.message} — proceeding with original`);
|
|
294
|
-
story.needsRefinement = false;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const huTask = buildHuTask(story);
|
|
299
|
-
|
|
300
|
-
// --- hu:start ---
|
|
301
|
-
emitProgress(emitter, makeEvent("hu:start", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
302
|
-
message: `Starting HU ${story.id}`,
|
|
303
|
-
detail: { huId: story.id, title: story.certified?.title || story.id, worktreePath: worktreePath || null }
|
|
304
|
-
}));
|
|
305
|
-
|
|
306
|
-
updateStoryStatus(batch, story.id, HU_STATUS.CODING);
|
|
307
|
-
await saveHuBatch(batchSessionId, batch);
|
|
308
|
-
await notifyStatus(story.id, HU_STATUS.CODING);
|
|
309
|
-
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
310
|
-
message: `HU ${story.id} status → coding`,
|
|
311
|
-
detail: { huId: story.id, status: HU_STATUS.CODING, timestamp: new Date().toISOString() }
|
|
312
|
-
}));
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
// PAR-E2 (KJC-TSK-0629): lanes handed a worktree must aim every git and
|
|
316
|
-
// filesystem touchpoint at it — laneOpts carries that path to the runner.
|
|
317
|
-
const iterResult = await runIterationFn(huTask, story, { worktreePath: worktreePath || null, laneSlot });
|
|
318
|
-
const approved = Boolean(iterResult?.approved);
|
|
319
|
-
|
|
320
|
-
// --- Transition to reviewing (post-coder, pre-reviewer evaluation) ---
|
|
321
|
-
updateStoryStatus(batch, story.id, HU_STATUS.REVIEWING);
|
|
322
|
-
await saveHuBatch(batchSessionId, batch);
|
|
323
|
-
await notifyStatus(story.id, HU_STATUS.REVIEWING);
|
|
324
|
-
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
325
|
-
message: `HU ${story.id} status → reviewing`,
|
|
326
|
-
detail: { huId: story.id, status: HU_STATUS.REVIEWING, timestamp: new Date().toISOString() }
|
|
327
|
-
}));
|
|
328
|
-
|
|
329
|
-
if (approved) {
|
|
330
|
-
updateStoryStatus(batch, story.id, HU_STATUS.DONE);
|
|
331
|
-
await saveHuBatch(batchSessionId, batch);
|
|
332
|
-
await notifyStatus(story.id, HU_STATUS.DONE);
|
|
333
|
-
// PR3: build + persist the per-HU outcome.
|
|
334
|
-
const okOutcome = buildHuOutcome({ story, iterResult, status: HU_STATUS.DONE, startedAt: huStartedAt, huBudgetEntries: sliceHuBudget() });
|
|
335
|
-
await notifyOutcome(story.id, okOutcome);
|
|
336
|
-
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
337
|
-
message: `HU ${story.id} status → done`,
|
|
338
|
-
detail: { huId: story.id, status: HU_STATUS.DONE, timestamp: new Date().toISOString() }
|
|
339
|
-
}));
|
|
340
|
-
emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
341
|
-
status: "ok",
|
|
342
|
-
message: `HU ${story.id} completed successfully`,
|
|
343
|
-
detail: { huId: story.id, approved: true, outcome: okOutcome }
|
|
344
|
-
}));
|
|
345
|
-
return { huId: story.id, approved: true, result: iterResult };
|
|
346
|
-
} else {
|
|
347
|
-
updateStoryStatus(batch, story.id, HU_STATUS.FAILED);
|
|
348
|
-
await saveHuBatch(batchSessionId, batch);
|
|
349
|
-
await notifyStatus(story.id, HU_STATUS.FAILED);
|
|
350
|
-
const failOutcome = buildHuOutcome({ story, iterResult, status: HU_STATUS.FAILED, startedAt: huStartedAt, huBudgetEntries: sliceHuBudget() });
|
|
351
|
-
await notifyOutcome(story.id, failOutcome);
|
|
352
|
-
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
353
|
-
message: `HU ${story.id} status → failed`,
|
|
354
|
-
detail: { huId: story.id, status: HU_STATUS.FAILED, timestamp: new Date().toISOString() }
|
|
355
|
-
}));
|
|
356
|
-
emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
357
|
-
status: "fail",
|
|
358
|
-
message: `HU ${story.id} failed`,
|
|
359
|
-
detail: { huId: story.id, approved: false, reason: iterResult?.reason, outcome: failOutcome }
|
|
360
|
-
}));
|
|
361
|
-
return { huId: story.id, approved: false, result: iterResult };
|
|
362
|
-
}
|
|
363
|
-
} catch (err) {
|
|
364
|
-
updateStoryStatus(batch, story.id, HU_STATUS.FAILED);
|
|
365
|
-
await saveHuBatch(batchSessionId, batch);
|
|
366
|
-
await notifyStatus(story.id, HU_STATUS.FAILED);
|
|
367
|
-
const errOutcome = buildHuOutcome({ story, iterResult: { error: err.message }, status: HU_STATUS.FAILED, startedAt: huStartedAt, huBudgetEntries: sliceHuBudget() });
|
|
368
|
-
await notifyOutcome(story.id, errOutcome);
|
|
369
|
-
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
370
|
-
message: `HU ${story.id} status → failed`,
|
|
371
|
-
detail: { huId: story.id, status: HU_STATUS.FAILED, timestamp: new Date().toISOString() }
|
|
372
|
-
}));
|
|
373
|
-
emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
374
|
-
status: "fail",
|
|
375
|
-
message: `HU ${story.id} threw: ${err.message}`,
|
|
376
|
-
detail: { huId: story.id, approved: false, error: err.message }
|
|
377
|
-
}));
|
|
378
|
-
return { huId: story.id, approved: false, error: err.message };
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Run all certified HUs as sub-pipelines in topological order.
|
|
384
|
-
*
|
|
385
|
-
* @param {object} params
|
|
386
|
-
* @param {object} params.huReviewerResult - stageResults.huReviewer (from pre-loop)
|
|
387
|
-
* @param {Function} params.runIterationFn - async (task) => { approved, result, ... }
|
|
388
|
-
* Callback that runs the full coder→sonar→reviewer iteration loop for a given task.
|
|
389
|
-
* Returns an object with at least { approved: boolean }.
|
|
390
|
-
* @param {object} params.emitter - Event emitter
|
|
391
|
-
* @param {object} params.eventBase - Base event fields
|
|
392
|
-
* @param {object} params.logger - Logger instance
|
|
393
|
-
* @returns {Promise<{approved: boolean, results: object[], blockedIds: string[]}>}
|
|
394
|
-
*/
|
|
395
|
-
export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitter, eventBase, logger, config = null, onStatusChange = null, onOutcome = null, budgetTracker = null }) {
|
|
396
|
-
const batchSessionId = huReviewerResult.batchSessionId;
|
|
397
|
-
let batch;
|
|
398
|
-
try {
|
|
399
|
-
batch = await loadHuBatch(batchSessionId);
|
|
400
|
-
} catch {
|
|
401
|
-
logger.warn("HU sub-pipeline: could not load batch, falling back to in-memory stories");
|
|
402
|
-
// Build a minimal batch from the stage result
|
|
403
|
-
batch = {
|
|
404
|
-
session_id: batchSessionId,
|
|
405
|
-
stories: huReviewerResult.stories || []
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Plan-driven runs: the plan JSON is the source of truth for the
|
|
410
|
-
// ENTIRE HU, NOT just status, NOT the on-disk hu-stories batch.
|
|
411
|
-
//
|
|
412
|
-
// PR #537 reconciled `status` and fixed the "All HUs completed
|
|
413
|
-
// successfully for zero work" bug. But status is only ONE field of
|
|
414
|
-
// the HU. The 2026-04-29 incident hit a different field — a user
|
|
415
|
-
// edited the plan JSON to fix a broken jq query inside the HU's
|
|
416
|
-
// `acceptance_tests`, but the persisted batch kept the broken
|
|
417
|
-
// version, so the acceptance runner used the broken test, the coder
|
|
418
|
-
// burned every iteration trying to make an irreparable test pass,
|
|
419
|
-
// and the HU failed.
|
|
420
|
-
//
|
|
421
|
-
// Treat the plan as authoritative for ALL content fields the user
|
|
422
|
-
// might edit between runs (acceptance_tests, title, scope,
|
|
423
|
-
// blocked_by, spec_section, original.text). In-flight runtime
|
|
424
|
-
// context that doesn't live in the plan (worktree path, refinement
|
|
425
|
-
// state, certification quality scores, context_requests) is
|
|
426
|
-
// preserved.
|
|
427
|
-
if (huReviewerResult.source?.plan === true && Array.isArray(huReviewerResult.stories)) {
|
|
428
|
-
// Fields the plan owns. Anything NOT in this set is preserved
|
|
429
|
-
// from the persisted batch (in-flight runtime state).
|
|
430
|
-
const PLAN_OWNED_FIELDS = [
|
|
431
|
-
"status",
|
|
432
|
-
"title",
|
|
433
|
-
"scope",
|
|
434
|
-
"blocked_by",
|
|
435
|
-
"acceptance_tests",
|
|
436
|
-
"acceptance_criteria",
|
|
437
|
-
"spec_section",
|
|
438
|
-
"task_type",
|
|
439
|
-
"certified", // canonical HU body
|
|
440
|
-
"original", // original text reference
|
|
441
|
-
];
|
|
442
|
-
const planById = new Map(huReviewerResult.stories.map((s) => [s.id, s]));
|
|
443
|
-
let statusChanges = 0;
|
|
444
|
-
let contentChanges = 0;
|
|
445
|
-
for (const persisted of batch.stories) {
|
|
446
|
-
const fromPlan = planById.get(persisted.id);
|
|
447
|
-
if (!fromPlan) continue;
|
|
448
|
-
for (const field of PLAN_OWNED_FIELDS) {
|
|
449
|
-
if (!(field in fromPlan)) continue;
|
|
450
|
-
const before = JSON.stringify(persisted[field]);
|
|
451
|
-
const after = JSON.stringify(fromPlan[field]);
|
|
452
|
-
if (before === after) continue;
|
|
453
|
-
persisted[field] = fromPlan[field];
|
|
454
|
-
if (field === "status") statusChanges += 1;
|
|
455
|
-
else contentChanges += 1;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
// Stories present in the plan but missing from disk → append.
|
|
459
|
-
const persistedIds = new Set(batch.stories.map((s) => s.id));
|
|
460
|
-
let appended = 0;
|
|
461
|
-
for (const fromPlan of huReviewerResult.stories) {
|
|
462
|
-
if (!persistedIds.has(fromPlan.id)) {
|
|
463
|
-
batch.stories.push(fromPlan);
|
|
464
|
-
appended += 1;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
const totalChanges = statusChanges + contentChanges + appended;
|
|
468
|
-
if (totalChanges > 0) {
|
|
469
|
-
const parts = [];
|
|
470
|
-
if (statusChanges > 0) parts.push(`${statusChanges} status`);
|
|
471
|
-
if (contentChanges > 0) parts.push(`${contentChanges} content field(s)`);
|
|
472
|
-
if (appended > 0) parts.push(`${appended} appended`);
|
|
473
|
-
logger.info(`HU sub-pipeline: reconciled from plan — ${parts.join(", ")} (disk batch was stale)`);
|
|
474
|
-
await saveHuBatch(batchSessionId, batch);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const certifiedStories = batch.stories.filter(s => s.status === HU_STATUS.CERTIFIED);
|
|
479
|
-
// KJC-BUG-0315 (#2065): a cycle has no valid order. The catch below fell back to
|
|
480
|
-
// the list order and ran it anyway; a cycle now stops the run and is named.
|
|
481
|
-
assertNoCycle(certifiedStories);
|
|
482
|
-
let orderedIds;
|
|
483
|
-
try {
|
|
484
|
-
orderedIds = topologicalSort(certifiedStories);
|
|
485
|
-
} catch { /* a dependency outside the certified batch (done or pending): keep the list order */
|
|
486
|
-
orderedIds = certifiedStories.map(s => s.id);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const results = [];
|
|
490
|
-
const blockedIds = [];
|
|
491
|
-
let allApproved = true;
|
|
492
|
-
|
|
493
|
-
// --- Group HUs into parallel batches ---
|
|
494
|
-
const parallelBatches = findParallelGroups(certifiedStories, orderedIds);
|
|
495
|
-
|
|
496
|
-
// Governance (KJC-TSK-0626): the dependency groups above used to launch
|
|
497
|
-
// UNBOUNDED via Promise.all — a plan with N independent HUs ran N full
|
|
498
|
-
// pipelines at once, with no cap, no shared budget and no scope checks.
|
|
499
|
-
// Now each group is partitioned into conflict-free chunks of at most
|
|
500
|
-
// max_parallel_hus (default 1 = fully sequential, today's safe path) and
|
|
501
|
-
// a shared limiter gates every launch against the PLAN budget.
|
|
502
|
-
const maxParallel = Math.max(1, Number(config?.session?.max_parallel_hus ?? 1));
|
|
503
|
-
const limiter = createParallelLimiter({
|
|
504
|
-
maxParallel,
|
|
505
|
-
budgetTracker,
|
|
506
|
-
planBudgetUsd: planBudgetUsd({ maxBudgetUsd: config?.max_budget_usd, maxParallel }),
|
|
507
|
-
});
|
|
508
|
-
let stopReason = null;
|
|
509
|
-
|
|
510
|
-
outer: for (const group of parallelBatches) {
|
|
511
|
-
for (const chunk of partitionConflictFree(batch.stories, group, maxParallel)) {
|
|
512
|
-
// Filter out HUs that were blocked by a failed dependency in a previous batch
|
|
513
|
-
const runnableIds = chunk.filter(id => {
|
|
514
|
-
const story = batch.stories.find(s => s.id === id);
|
|
515
|
-
return story && story.status !== HU_STATUS.BLOCKED;
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
if (runnableIds.length === 0) continue;
|
|
519
|
-
|
|
520
|
-
const blockReason = limiter.launchBlockReason();
|
|
521
|
-
if (blockReason) {
|
|
522
|
-
stopReason = blockReason;
|
|
523
|
-
allApproved = false;
|
|
524
|
-
logger.warn(`HU sub-pipeline stopped before launching ${runnableIds.join(", ")}: ${blockReason}`);
|
|
525
|
-
break outer;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Emit parallel batch start event
|
|
529
|
-
emitProgress(emitter, makeEvent("hu:parallel-start", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
530
|
-
message: `Starting parallel batch of ${runnableIds.length} HU(s): ${runnableIds.join(", ")}`,
|
|
531
|
-
detail: { batchIds: runnableIds, parallel: runnableIds.length > 1 }
|
|
532
|
-
}));
|
|
533
|
-
|
|
534
|
-
if (runnableIds.length === 1) {
|
|
535
|
-
// Single HU: run as before, no worktree needed
|
|
536
|
-
const singleResult = await runSingleHu({
|
|
537
|
-
storyId: runnableIds[0], batch, batchSessionId, runIterationFn,
|
|
538
|
-
emitter, eventBase, logger, config, results, onStatusChange, onOutcome, budgetTracker
|
|
539
|
-
});
|
|
540
|
-
results.push(singleResult);
|
|
541
|
-
if (!singleResult.approved) {
|
|
542
|
-
allApproved = false;
|
|
543
|
-
const newlyBlocked = blockDependents(batch, singleResult.huId);
|
|
544
|
-
blockedIds.push(...newlyBlocked);
|
|
545
|
-
// PR1 (live HU status): notify the plan JSON of every newly
|
|
546
|
-
// BLOCKED HU so the board moves them into the blocked column
|
|
547
|
-
// without waiting for the run to finish.
|
|
548
|
-
if (typeof onStatusChange === "function") {
|
|
549
|
-
for (const blockedHuId of newlyBlocked) {
|
|
550
|
-
try { await onStatusChange(blockedHuId, HU_STATUS.BLOCKED); }
|
|
551
|
-
catch (err) { logger?.warn?.(`onStatusChange(${blockedHuId}, blocked) failed: ${err.message}`); }
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
await saveHuBatch(batchSessionId, batch);
|
|
556
|
-
} else {
|
|
557
|
-
// Multiple HUs: create worktrees, run in parallel
|
|
558
|
-
const projectDir = config?.projectDir || process.cwd();
|
|
559
|
-
const worktrees = new Map();
|
|
560
|
-
// PAR-H (KJC-TSK-0631): every lane gets a stable numeric slot so
|
|
561
|
-
// services it starts can offset their ports (KJ_LANE_SLOT /
|
|
562
|
-
// KJ_PORT_OFFSET reach the coder and the acceptance tests).
|
|
563
|
-
const slotRegistryPath = join(getKarajanHome(), "worktree-slots.json");
|
|
564
|
-
const laneSlots = new Map();
|
|
565
|
-
|
|
566
|
-
// Create worktrees for each HU in the batch
|
|
567
|
-
for (const id of runnableIds) {
|
|
568
|
-
try {
|
|
569
|
-
const wtPath = await createWorktree(projectDir, id);
|
|
570
|
-
worktrees.set(id, wtPath);
|
|
571
|
-
// PAR-G (KJC-TSK-0630): a fresh worktree has no node_modules and
|
|
572
|
-
// no initialized submodules — make the lane operative before the
|
|
573
|
-
// coder lands. Best-effort: warnings never block the lane.
|
|
574
|
-
await bootstrapWorktree({ worktreePath: wtPath, setupCommand: config?.session?.worktree_setup || null, logger });
|
|
575
|
-
try {
|
|
576
|
-
const { slot } = await acquireSlot({ registryPath: slotRegistryPath, id: `${projectDir}::${id}` });
|
|
577
|
-
laneSlots.set(id, slot);
|
|
578
|
-
} catch (err) {
|
|
579
|
-
logger.warn(`Failed to acquire lane slot for HU ${id}: ${err.message} — lane runs without port offset`);
|
|
580
|
-
}
|
|
581
|
-
} catch (err) {
|
|
582
|
-
logger.warn(`Failed to create worktree for HU ${id}: ${err.message} — will run sequentially`);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Run all HUs in the batch concurrently, each holding a limiter slot
|
|
587
|
-
// (belt-and-braces: chunks are already ≤ maxParallel).
|
|
588
|
-
const batchPromises = runnableIds.map(async (storyId) => {
|
|
589
|
-
await limiter.acquire();
|
|
590
|
-
try {
|
|
591
|
-
return await runSingleHu({
|
|
592
|
-
storyId, batch, batchSessionId, runIterationFn,
|
|
593
|
-
emitter, eventBase, logger, config, results,
|
|
594
|
-
worktreePath: worktrees.get(storyId),
|
|
595
|
-
laneSlot: laneSlots.get(storyId) ?? null,
|
|
596
|
-
onStatusChange, onOutcome, budgetTracker
|
|
597
|
-
});
|
|
598
|
-
} finally {
|
|
599
|
-
limiter.release();
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
const batchResults = await Promise.all(batchPromises);
|
|
604
|
-
|
|
605
|
-
// Process results: merge successful worktrees sequentially, clean up failed ones
|
|
606
|
-
for (const res of batchResults) {
|
|
607
|
-
results.push(res);
|
|
608
|
-
if (res.approved && worktrees.has(res.huId)) {
|
|
609
|
-
try {
|
|
610
|
-
await mergeWorktree(projectDir, res.huId);
|
|
611
|
-
await releaseSlot({ registryPath: slotRegistryPath, id: `${projectDir}::${res.huId}` }).catch(() => {});
|
|
612
|
-
} catch (err) {
|
|
613
|
-
logger.warn(`Failed to merge worktree for HU ${res.huId}: ${err.message}`);
|
|
614
|
-
}
|
|
615
|
-
} else if (!res.approved) {
|
|
616
|
-
allApproved = false;
|
|
617
|
-
const newlyBlocked = blockDependents(batch, res.huId);
|
|
618
|
-
blockedIds.push(...newlyBlocked);
|
|
619
|
-
// PR1 (live HU status): same as the single-HU path — push
|
|
620
|
-
// BLOCKED transitions to the plan JSON immediately so the
|
|
621
|
-
// board shows them moving to the blocked column live.
|
|
622
|
-
if (typeof onStatusChange === "function") {
|
|
623
|
-
for (const blockedHuId of newlyBlocked) {
|
|
624
|
-
try { await onStatusChange(blockedHuId, HU_STATUS.BLOCKED); }
|
|
625
|
-
catch (err) { logger?.warn?.(`onStatusChange(${blockedHuId}, blocked) failed: ${err.message}`); }
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
// Clean up failed worktree
|
|
629
|
-
if (worktrees.has(res.huId)) {
|
|
630
|
-
try { await removeWorktree(projectDir, res.huId); } catch { /* ignore */ }
|
|
631
|
-
await releaseSlot({ registryPath: slotRegistryPath, id: `${projectDir}::${res.huId}` }).catch(() => {});
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
await saveHuBatch(batchSessionId, batch);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
return { approved: allApproved, results, blockedIds, ...(stopReason ? { stopReason } : {}) };
|
|
642
|
-
}
|