karajan-code 4.70.3 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/ai-surface.js +9 -7
- package/src/checks/config-files.js +8 -7
- 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 -140
- package/src/cli/register-plan.js +9 -100
- 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 -148
- package/src/commands/go.js +76 -55
- package/src/commands/harden.js +105 -86
- 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 +75 -48
- package/src/commands/review-gate.js +422 -339
- 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 +94 -25
- package/src/config/loader.js +34 -17
- package/src/config/overrides.js +2 -1
- package/src/config/secrets.js +36 -0
- package/src/config.js +1 -0
- package/src/environment/adr.js +16 -2
- package/src/environment/board-access.js +11 -9
- package/src/git/karajan-label.js +20 -8
- package/src/guards/main-guard.js +54 -0
- package/src/harden/branch-protection.js +36 -3
- package/src/harden/hook-templates.js +16 -1
- package/src/harden/sentinel/posttooluse.mjs +18 -6
- package/src/harden/sentinel/pretooluse-sentinel.mjs +81 -35
- package/src/harden/sentinel/sentinel-lib.mjs +149 -23
- package/src/harden/sentinel/sentinel-shell.mjs +64 -0
- package/src/harden/sentinel/sessionstart.mjs +3 -2
- package/src/harden/sentinel/stop.mjs +5 -4
- package/src/harden/workflow-engine.js +12 -1
- 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 +15 -7
- 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 +44 -55
- package/src/mcp/handlers/management-handlers.js +2 -42
- package/src/mcp/server-handlers.js +16 -22
- package/src/mcp/tools.js +5 -200
- 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 +56 -43
- package/src/rag/bootstrap-index.js +28 -54
- package/src/rag/migrate.js +1 -1
- package/src/rag/onnx-fallback.js +2 -2
- package/src/rag/pid-lease.js +70 -0
- package/src/rag/project-store.js +2 -12
- package/src/review/card-first.js +57 -46
- package/src/review/dual-review.js +5 -4
- package/src/review/gate-gitignore.js +23 -4
- package/src/review/gate-hook.js +47 -0
- package/src/review/one-shot-review.js +51 -37
- package/src/review/publish-pr.js +18 -3
- package/src/review/repo-docs.js +73 -0
- package/src/review/repo-stack.js +59 -0
- package/src/review/reviewer-fallback.js +34 -3
- 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 +2 -26
- package/src/roles/audit-role.js +106 -125
- package/src/skills/atril-skills.js +5 -4
- 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/agent-home.js +33 -0
- 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
package/src/commands/hu.js
CHANGED
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
import { addHu, updateHuStatus } from "../plan/plan-hu-ops.js";
|
|
9
9
|
import { generatePlanId } from "../plan/plan-id.js";
|
|
10
10
|
import { savePlan, listPlans, loadPlan } from "../plan/plan-store.js";
|
|
11
|
+
import { migrateLanePlans } from "../plan/lane-plans.js";
|
|
11
12
|
import { detectHostAgent } from "../utils/agent-detect.js";
|
|
12
13
|
import { readIdentity } from "../identity/store.js";
|
|
13
14
|
import { effectiveGitEmail } from "../identity/detect.js";
|
|
14
15
|
import { roleSkills } from "../skills/role-skills.js";
|
|
15
16
|
import { readBoardConfig, setBoardBackend } from "../hu/board-config.js";
|
|
16
17
|
import { createHu, listHus, claim, setStatus, assertNotHeld, isOffline } from "../hu/github-board.js";
|
|
17
|
-
import {
|
|
18
|
+
import { pinGhIdentity } from "../hu/gh-identity.js";
|
|
18
19
|
import { listWithCache, queueMove, flushQueue } from "../hu/team-board-cache.js";
|
|
19
20
|
import { writeTeamMirror } from "../hu/team-board-mirror.js";
|
|
21
|
+
import { startAction } from "./hu-start.js";
|
|
20
22
|
|
|
21
23
|
// KJC-TSK-0661 (Jorge's friction): every HU records WHO created it — the
|
|
22
24
|
// board must never show work nobody can trace. Host agent env → that agent;
|
|
@@ -58,11 +60,14 @@ function linesOf(v) {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/** KJC-TSK-1163: kj hu add on the shared board; the same no-duplicates rule, checked against the issues. */
|
|
61
|
-
async function addToTeamBoard({ repo, title, flags, deps, emit }) {
|
|
63
|
+
async function addToTeamBoard({ projectDir, repo, title, flags, deps, emit }) {
|
|
62
64
|
if (flags.id) throw new Error(`--id is not used with the shared board: GitHub gives each HU its id (the issue number)`);
|
|
63
65
|
let hu;
|
|
66
|
+
let existing;
|
|
67
|
+
const ref = flags.ref ? String(flags.ref).trim() : null;
|
|
64
68
|
try {
|
|
65
|
-
|
|
69
|
+
existing = await (deps.listTeamHus ?? listHus)({ repo });
|
|
70
|
+
const { identical, similar } = findTitleMatches(title, existing);
|
|
66
71
|
if (identical.length > 0) {
|
|
67
72
|
throw new Error(`An HU with this title already exists: ${identical[0].short_id} (status: ${identical[0].status}). Work that card or move it with: kj hu move ${identical[0].short_id} <status>`);
|
|
68
73
|
}
|
|
@@ -71,13 +76,15 @@ async function addToTeamBoard({ repo, title, flags, deps, emit }) {
|
|
|
71
76
|
console.warn("⚠ possible duplicate — check before working it:\n" + candidates);
|
|
72
77
|
}
|
|
73
78
|
hu = await (deps.createTeamHu ?? createHu)({
|
|
74
|
-
repo, title, acceptanceCriteria: [...linesOf(flags.ac), ...linesOf(flags.criteria)], tests: linesOf(flags.tests), ref
|
|
79
|
+
repo, title, acceptanceCriteria: [...linesOf(flags.ac), ...linesOf(flags.criteria)], tests: linesOf(flags.tests), ref,
|
|
75
80
|
});
|
|
76
81
|
} catch (err) {
|
|
77
82
|
// KJC-TSK-1165: an id is only ever minted by GitHub; nothing is created offline.
|
|
78
83
|
if (isOffline(err)) throw new Error(`creating a HU needs the network: GitHub gives it its id and no local one is made up (${err.message})`, { cause: err });
|
|
79
84
|
throw err;
|
|
80
85
|
}
|
|
86
|
+
// KJC-BUG-0443: the gates read the mirror; the new HU is in it before the next kj hu list.
|
|
87
|
+
await (deps.writeMirror ?? writeTeamMirror)(projectDir, [...existing, { ...hu, claimed_by: null, ref }], { repo });
|
|
81
88
|
return emit({ ...hu, board: "github", repo }, `✓ HU created: ${hu.short_id} (pending) in the shared board, issue #${hu.number} of ${repo}`);
|
|
82
89
|
}
|
|
83
90
|
|
|
@@ -176,6 +183,61 @@ export async function backlogPlan(projectDir) {
|
|
|
176
183
|
return loadPlan(projectDir, planId);
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
// KJC-BUG-0305 (issue #2042): the HU Board is always kj's board. An external
|
|
187
|
+
// tracker coexists with it: each HU points at its replica there (--ref).
|
|
188
|
+
const externalBoardOf = (config) => (config?.state_backend === "external" ? (config?.board?.name || "the external board") : null);
|
|
189
|
+
|
|
190
|
+
// KJC-TSK-1166 (TEAM-GH-E): the HUs created before the shared board, moved to it once.
|
|
191
|
+
function migrateAction({ projectDir, board, flags, deps, emit, who }) {
|
|
192
|
+
if (flags.to !== "github") throw new Error("usage: kj hu migrate --to github [--dry-run]");
|
|
193
|
+
if (board.backend !== "github") throw new Error("the shared board is off in this repo: turn it on first with kj hu backend github");
|
|
194
|
+
return migrateToTeamBoard({ projectDir, repo: board.repo, who, dryRun: Boolean(flags.dryRun), deps, emit });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function backendAction({ projectDir, args, deps, emit }) {
|
|
198
|
+
if (!args[0]) throw new Error("usage: kj hu backend <github | local>");
|
|
199
|
+
const r = await (deps.setBackend ?? setBoardBackend)({ projectDir, backend: args[0] });
|
|
200
|
+
const where = r.backend === "github" ? `GitHub Issues of ${r.repo}` : "the local board";
|
|
201
|
+
const files = r.gitignoreChanged ? `${r.file} and .gitignore` : r.file;
|
|
202
|
+
return emit(r, `✓ HU Board: ${where} — commit ${files} in a PR so the whole team inherits it`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// #2566: plans a lane saved under its own slug move to the main checkout's; loud, never blocking.
|
|
206
|
+
async function migrateLanePlansLoudly(projectDir) {
|
|
207
|
+
const { moved, kept } = await migrateLanePlans(projectDir)
|
|
208
|
+
.catch((err) => { console.warn(`⚠ plans from lanes not migrated: ${err.message}`); return { moved: [], kept: [] }; });
|
|
209
|
+
const keptNote = kept.length ? `, ${kept.length} kept (name taken, compare and delete): ${kept.join(", ")}` : "";
|
|
210
|
+
if (moved.length || kept.length) console.warn(`plans from lanes: ${moved.length} moved to the main checkout${keptNote}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function listTeam({ projectDir, repo, flags, deps }) {
|
|
214
|
+
const { source, rows: hus, updatedAt } = await listWithCache({ projectDir, repo, list: deps.listTeamHus ?? listHus });
|
|
215
|
+
if (source === "cache") console.warn(`⚠ GitHub did not answer: this is the local copy of the board from ${updatedAt}`);
|
|
216
|
+
// KJC-TSK-1166: what GitHub answered is what kj board shows, as a read-only plan.
|
|
217
|
+
else await writeTeamMirror(projectDir, hus, { repo });
|
|
218
|
+
const rows = hus.map((h) => ({ ...h, id: h.short_id, plan: "github" }));
|
|
219
|
+
if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
|
|
220
|
+
for (const r of rows) {
|
|
221
|
+
const held = r.claimed_by ? " · held by " + r.claimed_by : "";
|
|
222
|
+
console.log(`${r.short_id.padEnd(28)} ${r.status.padEnd(8)} ${r.title} [${repo}${held}]`);
|
|
223
|
+
}
|
|
224
|
+
if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
|
|
225
|
+
return rows;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function moveTeamOrQueue({ projectDir, repo, args, flags, deps, emit, who }) {
|
|
229
|
+
try {
|
|
230
|
+
return await moveOnTeamBoard({ repo, args, flags, deps, emit, who });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
// Offline: the move waits in the queue; GitHub still decides when it is applied.
|
|
233
|
+
if (!isOffline(err)) throw err;
|
|
234
|
+
const number = Number(/^(?:HU-|#)(\d+)$/i.exec(args[0])[1]);
|
|
235
|
+
queueMove({ projectDir, number, status: args[1], who, release: Boolean(flags.release) });
|
|
236
|
+
console.warn(`⚠ GitHub did not answer: HU-${number} → ${args[1]} queued, applied when GitHub answers again`);
|
|
237
|
+
return emit({ id: `HU-${number}`, status: args[1], queued: true }, `⏳ HU-${number} → ${args[1]} (queued)`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
179
241
|
// KJC-TSK-1047 (TEAM-A): who works this clone, the one a started card is reserved to.
|
|
180
242
|
const workerOf = (projectDir) => readIdentity(projectDir)?.gh_user || effectiveGitEmail(projectDir) || null;
|
|
181
243
|
|
|
@@ -183,238 +245,243 @@ export async function huCommand({ config = null, action, args = [], flags = {},
|
|
|
183
245
|
const projectDir = config?.projectDir || process.cwd();
|
|
184
246
|
const emit = (obj, human) => { console.log(flags.json ? JSON.stringify(obj) : human); return obj; };
|
|
185
247
|
|
|
186
|
-
// KJC-BUG-0305 (issue #2042): the HU Board is always kj's board. An external
|
|
187
|
-
// tracker coexists with it: each HU points at its replica there (--ref).
|
|
188
|
-
const externalBoard = config?.state_backend === "external" ? (config?.board?.name || "the external board") : null;
|
|
189
|
-
|
|
190
248
|
// KJC-TSK-1163/1164 (ADR 0025): the repo's board; with the shared one on, add/list/move go to GitHub.
|
|
191
249
|
// The repo turns its board on once; the file travels in a PR. Before reading it: it may be what is fixed.
|
|
192
|
-
if (action === "backend") {
|
|
193
|
-
if (!args[0]) throw new Error("usage: kj hu backend <github | local>");
|
|
194
|
-
const r = await (deps.setBackend ?? setBoardBackend)({ projectDir, backend: args[0] });
|
|
195
|
-
const where = r.backend === "github" ? `GitHub Issues of ${r.repo}` : "the local board";
|
|
196
|
-
const files = r.gitignoreChanged ? `${r.file} and .gitignore` : r.file;
|
|
197
|
-
return emit(r, `✓ HU Board: ${where} — commit ${files} in a PR so the whole team inherits it`);
|
|
198
|
-
}
|
|
250
|
+
if (action === "backend") return backendAction({ projectDir, args, deps, emit });
|
|
199
251
|
const board = await (deps.boardConfig ?? readBoardConfig)(projectDir);
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
|
|
252
|
+
const shared = board.backend === "github";
|
|
253
|
+
// KJC-TSK-1186 + KJC-BUG-0406: with an identity declared, every gh call of the command goes out as
|
|
254
|
+
// that account (GH_TOKEN pinned before the first one), whatever another session switches to.
|
|
255
|
+
if (shared) (deps.pinGhIdentity ?? pinGhIdentity)({ projectDir });
|
|
203
256
|
const who = deps.who === undefined ? workerOf(projectDir) : deps.who;
|
|
204
|
-
|
|
205
|
-
if (action === "migrate") {
|
|
206
|
-
if (flags.to !== "github") throw new Error("usage: kj hu migrate --to github [--dry-run]");
|
|
207
|
-
if (board.backend !== "github") throw new Error("the shared board is off in this repo: turn it on first with kj hu backend github");
|
|
208
|
-
return migrateToTeamBoard({ projectDir, repo: board.repo, who, dryRun: Boolean(flags.dryRun), deps, emit });
|
|
209
|
-
}
|
|
257
|
+
if (action === "migrate") return migrateAction({ projectDir, board, flags, deps, emit, who });
|
|
210
258
|
// KJC-TSK-1165 (TEAM-GH-D): moves made without network go first, once GitHub answers.
|
|
211
|
-
if (
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
if (source === "cache") console.warn(`⚠ GitHub did not answer: this is the local copy of the board from ${updatedAt}`);
|
|
215
|
-
// KJC-TSK-1166: what GitHub answered is what kj board shows, as a read-only plan.
|
|
216
|
-
else await writeTeamMirror(projectDir, hus, { repo: board.repo });
|
|
217
|
-
const rows = hus.map((h) => ({ ...h, id: h.short_id, plan: "github" }));
|
|
218
|
-
if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
|
|
219
|
-
for (const r of rows) {
|
|
220
|
-
const held = r.claimed_by ? " · held by " + r.claimed_by : "";
|
|
221
|
-
console.log(`${r.short_id.padEnd(28)} ${r.status.padEnd(8)} ${r.title} [${board.repo}${held}]`);
|
|
222
|
-
}
|
|
223
|
-
if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
|
|
224
|
-
return rows;
|
|
225
|
-
}
|
|
259
|
+
if (shared) await applyQueuedMoves({ projectDir, repo: board.repo, deps });
|
|
260
|
+
if (action === "list") await migrateLanePlansLoudly(projectDir);
|
|
261
|
+
if (shared && action === "list") return listTeam({ projectDir, repo: board.repo, flags, deps });
|
|
226
262
|
// KJC-BUG-0392: a zero-padded id (HU-0104) is a local HU from before the shared board: it stays local.
|
|
227
263
|
const legacyLocal = /^HU-0\d+$/i.test(String(args[0] ?? ""));
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
264
|
+
if (shared && action === "move" && !legacyLocal) return moveTeamOrQueue({ projectDir, repo: board.repo, args, flags, deps, emit, who });
|
|
265
|
+
|
|
266
|
+
// KJC-TSK-1257: one function per action on the local board (add also routes to the shared one).
|
|
267
|
+
if (action === "list") return listLocal({ projectDir, flags });
|
|
268
|
+
if (action === "add") return addAction({ projectDir, board, args, flags, deps, emit, externalBoard: externalBoardOf(config) });
|
|
269
|
+
if (action === "move") return moveLocal({ projectDir, args, flags, deps, emit });
|
|
270
|
+
// KJC-TSK-1372 (#2853): card, lane, running, in that order.
|
|
271
|
+
if (action === "start") {
|
|
272
|
+
const findCard = deps.findCard ?? (shared ? findSharedCard(board.repo, deps) : findLocalCard(projectDir));
|
|
273
|
+
const move = () => huCommand({ config, action: "move", args: [args[0], "running"], flags: { json: flags.json }, deps });
|
|
274
|
+
return startAction({ config, args, flags, deps: { ...deps, findCard }, move, who, emit });
|
|
239
275
|
}
|
|
240
276
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
for (const r of rows) {
|
|
260
|
-
const reserved = r.status === "running" && r.claimed_by ? `reserved by ${r.claimed_by}` : null;
|
|
261
|
-
const suffixes = [r.created_at?.slice(0, 10), r.external_ref, reserved].filter(Boolean).map((s) => ` · ${s}`).join("");
|
|
262
|
-
const origin = `${r.plan} · by ${r.created_by || "?"}${suffixes}`;
|
|
263
|
-
console.log(`${(r.short_id || r.id).padEnd(28)} ${r.status.padEnd(8)} ${r.title} [${origin}]`);
|
|
277
|
+
throw new Error(`unknown action "${action}" — use: add | move | list | start | backend`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function listLocal({ projectDir, flags }) {
|
|
281
|
+
const plans = await listPlans(projectDir);
|
|
282
|
+
const rows = [];
|
|
283
|
+
for (const meta of plans) {
|
|
284
|
+
const plan = await loadPlan(projectDir, meta.planId);
|
|
285
|
+
for (const h of plan.hus || []) {
|
|
286
|
+
rows.push({
|
|
287
|
+
id: h.id, short_id: h.short_id, title: h.title, status: h.status,
|
|
288
|
+
plan: plan.alias || plan.planId,
|
|
289
|
+
// KJC-TSK-0661: provenance — where a card came from is part of
|
|
290
|
+
// the card. Older HUs predate the stamp: shown as "?".
|
|
291
|
+
created_by: h.created_by || null, created_at: h.createdAt || null,
|
|
292
|
+
external_ref: h.external_ref || null,
|
|
293
|
+
claimed_by: h.claimed_by || null, claimed_at: h.claimed_at || null,
|
|
294
|
+
});
|
|
264
295
|
}
|
|
265
|
-
if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
|
|
266
|
-
else console.log("→ work one: ask your agent, or kj run --plan <plan> · discard one: kj hu move <id> skipped");
|
|
267
|
-
return rows;
|
|
268
296
|
}
|
|
297
|
+
if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
|
|
298
|
+
for (const r of rows) {
|
|
299
|
+
const reserved = r.status === "running" && r.claimed_by ? `reserved by ${r.claimed_by}` : null;
|
|
300
|
+
const suffixes = [r.created_at?.slice(0, 10), r.external_ref, reserved].filter(Boolean).map((s) => ` · ${s}`).join("");
|
|
301
|
+
const origin = `${r.plan} · by ${r.created_by || "?"}${suffixes}`;
|
|
302
|
+
console.log(`${(r.short_id || r.id).padEnd(28)} ${r.status.padEnd(8)} ${r.title} [${origin}]`);
|
|
303
|
+
}
|
|
304
|
+
if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
|
|
305
|
+
else console.log("→ work one: ask your agent · discard one: kj hu move <id> skipped");
|
|
306
|
+
return rows;
|
|
307
|
+
}
|
|
269
308
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
if (similar.length > 0) {
|
|
291
|
-
const plural = similar.length === 1 ? "" : "s";
|
|
292
|
-
const candidates = similar.map((h) => ` · ${h.short_id || h.id} (${h.status}, plan: ${h.plan}) "${h.title}"`).join("\n");
|
|
293
|
-
console.warn(`⚠ possible duplicate${plural} — check before working it:\n${candidates}`);
|
|
294
|
-
}
|
|
295
|
-
const plan = await backlogPlan(projectDir);
|
|
296
|
-
/** Lowest free HU-#### in this plan — ids are permanent, never reused. */
|
|
297
|
-
const nextHuRef = (p) => {
|
|
298
|
-
const taken = new Set((p.hus || []).map((h) => h.short_id).filter(Boolean));
|
|
299
|
-
let n = 1;
|
|
300
|
-
while (taken.has(`HU-${String(n).padStart(4, "0")}`)) n += 1;
|
|
301
|
-
return `HU-${String(n).padStart(4, "0")}`;
|
|
302
|
-
};
|
|
303
|
-
// KJC-TSK-0669 (absolute rule): cards are permanent. The delete-and-
|
|
304
|
-
// recreate "fix" an agent improvises loses history and duplicates ids —
|
|
305
|
-
// refuse it with the norm spelled out.
|
|
306
|
-
if (flags.id) {
|
|
307
|
-
const dup = (plan.hus || []).find((h) => h.short_id === flags.id);
|
|
308
|
-
if (dup) {
|
|
309
|
-
throw new Error(
|
|
310
|
-
`HU "${flags.id}" already exists (status: ${dup.status}). HUs are never deleted or recreated — `
|
|
311
|
-
+ `update its state with: kj hu move ${flags.id} <pending|running|done|failed|skipped>, `
|
|
312
|
-
+ `or discard it with: kj hu move ${flags.id} skipped`
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
// KJC-TSK-0678 (issue #1296): complete HUs in ONE call. --ac / --tests
|
|
317
|
-
// are repeatable AND accept multiline strings; --criteria stays as the
|
|
318
|
-
// legacy single-criterion alias.
|
|
319
|
-
const TASK_TYPES = ["sw", "infra", "doc", "add-tests", "refactor"];
|
|
320
|
-
if (flags.taskType && !TASK_TYPES.includes(flags.taskType)) {
|
|
321
|
-
throw new Error(`invalid --task-type "${flags.taskType}" — valid: ${TASK_TYPES.join(", ")}`);
|
|
322
|
-
}
|
|
323
|
-
const splitLines = linesOf;
|
|
324
|
-
const hu = addHu(plan, {
|
|
325
|
-
title,
|
|
326
|
-
// KJC-BUG-0189: the card-first gate demands a branch that references a
|
|
327
|
-
// card, and it is this command the gate sends the agent to. The
|
|
328
|
-
// canonical plan id (hu_plan-<stamp>-<rand>_001) does not match
|
|
329
|
-
// CARD_REF_RE, so without an explicit --id the card kj just created
|
|
330
|
-
// could not satisfy kj's own gate. Sequential HU-#### does.
|
|
331
|
-
short_id: flags.id || nextHuRef(plan),
|
|
332
|
-
acceptance_criteria: [...splitLines(flags.ac), ...splitLines(flags.criteria)],
|
|
333
|
-
acceptance_tests: splitLines(flags.tests),
|
|
334
|
-
scope: flags.scope || null,
|
|
335
|
-
task_type: flags.taskType || undefined,
|
|
336
|
-
});
|
|
337
|
-
hu.created_by = creatorLabel();
|
|
338
|
-
if (flags.ref) hu.external_ref = String(flags.ref).trim();
|
|
339
|
-
await savePlan(projectDir, plan);
|
|
340
|
-
if (externalBoard && !hu.external_ref) {
|
|
341
|
-
console.log(`→ replicate it in ${externalBoard} through your agent's MCP/tools and link both: kj hu add ... --ref <id in ${externalBoard}>`);
|
|
342
|
-
}
|
|
343
|
-
const missing = [];
|
|
344
|
-
if (hu.acceptance_criteria.length === 0) missing.push("acceptance criteria (--ac \"...\")");
|
|
345
|
-
if (hu.acceptance_tests.length === 0) missing.push("tests (--tests \"...\")");
|
|
346
|
-
if (missing.length > 0) {
|
|
347
|
-
console.warn(`⚠ incomplete spec — kj run's spec review will flag it. Missing: ${missing.join(", ")}`);
|
|
348
|
-
}
|
|
349
|
-
return emit({
|
|
350
|
-
id: hu.id, short_id: hu.short_id, status: hu.status, created_by: hu.created_by,
|
|
351
|
-
acceptance_criteria: hu.acceptance_criteria, acceptance_tests: hu.acceptance_tests,
|
|
352
|
-
scope: hu.scope, task_type: hu.task_type, external_ref: hu.external_ref || null,
|
|
353
|
-
}, `✓ HU created: ${hu.short_id || hu.id} (pending, by ${hu.created_by}) — it shows up in \`kj board\``);
|
|
309
|
+
/** Cross-plan title dedup (KJC-TSK-0675) — checked BEFORE creating anything. */
|
|
310
|
+
async function refuseDuplicateTitle(projectDir, title) {
|
|
311
|
+
const allHus = [];
|
|
312
|
+
for (const meta of await listPlans(projectDir)) {
|
|
313
|
+
const p = await loadPlan(projectDir, meta.planId);
|
|
314
|
+
for (const h of p.hus || []) allHus.push({ ...h, plan: p.alias || p.name || p.planId });
|
|
315
|
+
}
|
|
316
|
+
const { identical, similar } = findTitleMatches(title, allHus);
|
|
317
|
+
if (identical.length > 0) {
|
|
318
|
+
const ref = identical[0];
|
|
319
|
+
throw new Error(
|
|
320
|
+
`An HU with this title already exists: ${ref.short_id || ref.id} (status: ${ref.status}, plan: ${ref.plan}). `
|
|
321
|
+
+ `HUs are never duplicated — work that card, move it with: kj hu move ${ref.short_id || ref.id} <status>, `
|
|
322
|
+
+ `or run kj hu list before adding.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (similar.length > 0) {
|
|
326
|
+
const plural = similar.length === 1 ? "" : "s";
|
|
327
|
+
const candidates = similar.map((h) => ` · ${h.short_id || h.id} (${h.status}, plan: ${h.plan}) "${h.title}"`).join("\n");
|
|
328
|
+
console.warn(`⚠ possible duplicate${plural} — check before working it:\n${candidates}`);
|
|
354
329
|
}
|
|
330
|
+
}
|
|
355
331
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
332
|
+
/** Lowest free HU-#### in this plan — ids are permanent, never reused. */
|
|
333
|
+
function nextHuRef(plan) {
|
|
334
|
+
const taken = new Set((plan.hus || []).map((h) => h.short_id).filter(Boolean));
|
|
335
|
+
let n = 1;
|
|
336
|
+
while (taken.has(`HU-${String(n).padStart(4, "0")}`)) n += 1;
|
|
337
|
+
return `HU-${String(n).padStart(4, "0")}`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// KJC-TSK-0678 (issue #1296): complete HUs in ONE call. --ac / --tests
|
|
341
|
+
// are repeatable AND accept multiline strings; --criteria stays as the
|
|
342
|
+
// legacy single-criterion alias.
|
|
343
|
+
const TASK_TYPES = ["sw", "infra", "doc", "add-tests", "refactor"];
|
|
344
|
+
|
|
345
|
+
function assertAddFlags(plan, flags) {
|
|
346
|
+
// KJC-TSK-0669 (absolute rule): cards are permanent. The delete-and-
|
|
347
|
+
// recreate "fix" an agent improvises loses history and duplicates ids —
|
|
348
|
+
// refuse it with the norm spelled out.
|
|
349
|
+
const dup = flags.id ? (plan.hus || []).find((h) => h.short_id === flags.id) : null;
|
|
350
|
+
if (dup) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`HU "${flags.id}" already exists (status: ${dup.status}). HUs are never deleted or recreated — `
|
|
353
|
+
+ `update its state with: kj hu move ${flags.id} <pending|running|done|failed|skipped>, `
|
|
354
|
+
+ `or discard it with: kj hu move ${flags.id} skipped`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (flags.taskType && !TASK_TYPES.includes(flags.taskType)) {
|
|
358
|
+
throw new Error(`invalid --task-type "${flags.taskType}" — valid: ${TASK_TYPES.join(", ")}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function addAction({ projectDir, board, args, flags, deps, emit, externalBoard }) {
|
|
363
|
+
const title = args[0];
|
|
364
|
+
if (!title || !title.trim()) throw new Error("kj hu add requires a title: kj hu add \"<story>\"");
|
|
365
|
+
// KJC-TSK-1163 (ADR 0025): with the shared board on, the HU is an issue and GitHub gives its id.
|
|
366
|
+
if (board.backend === "github") return addToTeamBoard({ projectDir, repo: board.repo, title, flags, deps, emit });
|
|
367
|
+
await refuseDuplicateTitle(projectDir, title);
|
|
368
|
+
const plan = await backlogPlan(projectDir);
|
|
369
|
+
assertAddFlags(plan, flags);
|
|
370
|
+
const hu = addHu(plan, {
|
|
371
|
+
title,
|
|
372
|
+
// KJC-BUG-0189: the card-first gate demands a branch that references a
|
|
373
|
+
// card, and it is this command the gate sends the agent to. The
|
|
374
|
+
// canonical plan id (hu_plan-<stamp>-<rand>_001) does not match
|
|
375
|
+
// CARD_REF_RE, so without an explicit --id the card kj just created
|
|
376
|
+
// could not satisfy kj's own gate. Sequential HU-#### does.
|
|
377
|
+
short_id: flags.id || nextHuRef(plan),
|
|
378
|
+
acceptance_criteria: [...linesOf(flags.ac), ...linesOf(flags.criteria)],
|
|
379
|
+
acceptance_tests: linesOf(flags.tests),
|
|
380
|
+
scope: flags.scope || null,
|
|
381
|
+
task_type: flags.taskType || undefined,
|
|
382
|
+
});
|
|
383
|
+
hu.created_by = creatorLabel();
|
|
384
|
+
if (flags.ref) hu.external_ref = String(flags.ref).trim();
|
|
385
|
+
await savePlan(projectDir, plan);
|
|
386
|
+
if (externalBoard && !hu.external_ref) {
|
|
387
|
+
console.log(`→ replicate it in ${externalBoard} through your agent's MCP/tools and link both: kj hu add ... --ref <id in ${externalBoard}>`);
|
|
388
|
+
}
|
|
389
|
+
const missing = [];
|
|
390
|
+
if (hu.acceptance_criteria.length === 0) missing.push("acceptance criteria (--ac \"...\")");
|
|
391
|
+
if (hu.acceptance_tests.length === 0) missing.push("tests (--tests \"...\")");
|
|
392
|
+
if (missing.length > 0) {
|
|
393
|
+
console.warn(`⚠ incomplete spec — kj run's spec review will flag it. Missing: ${missing.join(", ")}`);
|
|
394
|
+
}
|
|
395
|
+
return emit({
|
|
396
|
+
id: hu.id, short_id: hu.short_id, status: hu.status, created_by: hu.created_by,
|
|
397
|
+
acceptance_criteria: hu.acceptance_criteria, acceptance_tests: hu.acceptance_tests,
|
|
398
|
+
scope: hu.scope, task_type: hu.task_type, external_ref: hu.external_ref || null,
|
|
399
|
+
}, `✓ HU created: ${hu.short_id || hu.id} (pending, by ${hu.created_by}) — it shows up in \`kj board\``);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Exact canonical id wins outright (it IS the disambiguator); only then
|
|
404
|
+
* fall back to short_id matches — which can repeat between plans, so
|
|
405
|
+
* silently moving the first hit could move the wrong card.
|
|
406
|
+
*/
|
|
407
|
+
async function findHuToMove(projectDir, ref) {
|
|
408
|
+
const byId = [];
|
|
409
|
+
const byShort = [];
|
|
410
|
+
const running = [];
|
|
411
|
+
for (const meta of await listPlans(projectDir)) {
|
|
412
|
+
const plan = await loadPlan(projectDir, meta.planId);
|
|
413
|
+
for (const hu of plan.hus || []) {
|
|
414
|
+
if (hu.id === ref) byId.push({ plan, hu });
|
|
415
|
+
else if (hu.short_id === ref) byShort.push({ plan, hu });
|
|
416
|
+
if (hu.status === "running") running.push(hu);
|
|
415
417
|
}
|
|
416
|
-
return emit({ id: hu.id, status, claimed_by: hu.claimed_by ?? null }, `✓ ${label} → ${status}`);
|
|
417
418
|
}
|
|
419
|
+
const matches = byId.length > 0 ? byId : byShort;
|
|
420
|
+
if (matches.length === 0) throw new Error(`HU "${ref}" not found — see kj hu list`);
|
|
421
|
+
if (matches.length > 1) {
|
|
422
|
+
const ids = matches.map((m) => m.hu.id).join(", ");
|
|
423
|
+
throw new Error(`"${ref}" is ambiguous (${matches.length} matches: ${ids}) — use the full id`);
|
|
424
|
+
}
|
|
425
|
+
return { ...matches[0], running };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// KJC-TSK-1372: a card as `kj hu start` reads it, from either board; null when there is none.
|
|
429
|
+
const findSharedCard = (repo, deps) => async (ref) => {
|
|
430
|
+
const rows = await (deps.listTeamHus ?? listHus)({ repo });
|
|
431
|
+
const row = rows.find((r) => r.short_id.toLowerCase() === String(ref).toLowerCase());
|
|
432
|
+
return row ? { id: row.short_id, title: row.title, status: row.status, claimed_by: row.claimed_by } : null;
|
|
433
|
+
};
|
|
434
|
+
const findLocalCard = (projectDir) => async (ref) => {
|
|
435
|
+
const found = await findHuToMove(projectDir, ref).catch((err) => {
|
|
436
|
+
if (/not found/.test(err.message)) return null;
|
|
437
|
+
throw err;
|
|
438
|
+
});
|
|
439
|
+
if (!found) return null;
|
|
440
|
+
const { hu } = found;
|
|
441
|
+
return { id: hu.short_id || hu.id, title: hu.title, status: hu.status, claimed_by: hu.status === "running" ? hu.claimed_by ?? null : null };
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
async function moveLocal({ projectDir, args, flags, deps, emit }) {
|
|
445
|
+
const [ref, status] = args;
|
|
446
|
+
if (!ref || !status) throw new Error("usage: kj hu move <id> <status>");
|
|
447
|
+
if (!HU_STATUSES.includes(status)) {
|
|
448
|
+
throw new Error(`invalid status "${status}" — valid: ${HU_STATUSES.join(", ")}`);
|
|
449
|
+
}
|
|
450
|
+
const { plan, hu, running } = await findHuToMove(projectDir, ref);
|
|
451
|
+
// KJC-TSK-0688 (MG-C): the nudge fires AT the deviation — a second
|
|
452
|
+
// concurrent task belongs in its own lane, not in the shared tree.
|
|
453
|
+
const others = running.filter((r) => r.id !== hu.id);
|
|
454
|
+
if (status === "running" && hu.status !== "running" && others.length > 0) {
|
|
455
|
+
const slug = String(hu.short_id || hu.id).toLowerCase();
|
|
456
|
+
console.warn(
|
|
457
|
+
`⚠ ${others.map((r) => r.short_id || r.id).join(", ")} ${others.length === 1 ? "is" : "are"} already running — `
|
|
458
|
+
+ `a second concurrent task should live in its own lane: kj worktree start ${slug}`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
// KJC-TSK-1047 (TEAM-A): a running card reserved by someone else moves only with --release.
|
|
462
|
+
const who = deps.who === undefined ? workerOf(projectDir) : deps.who;
|
|
463
|
+
const label = hu.short_id || hu.id;
|
|
464
|
+
// An unidentified caller is not the one who reserved it.
|
|
465
|
+
const takenOver = hu.status === "running" && hu.claimed_by && hu.claimed_by !== who;
|
|
466
|
+
if (takenOver && !flags.release) {
|
|
467
|
+
throw new Error(`${label} is reserved by ${hu.claimed_by} since ${hu.claimed_at} — releasing it is explicit: kj hu move ${ref} ${status} --release`);
|
|
468
|
+
}
|
|
469
|
+
const starting = status === "running" && hu.status !== "running";
|
|
470
|
+
// A running card nobody reserved is reserved by who moves it there again, once kj knows who.
|
|
471
|
+
const unclaimedRunning = status === "running" && !hu.claimed_by && who;
|
|
472
|
+
updateHuStatus(plan, hu.id, status);
|
|
473
|
+
// Starting it, or releasing it to any status, rewrites the reservation: who did it, or nobody.
|
|
474
|
+
if (starting || takenOver || unclaimedRunning) Object.assign(hu, { claimed_by: who ?? null, claimed_at: who ? new Date().toISOString() : null });
|
|
475
|
+
if (starting && !who) console.warn(`⚠ ${label} is not reserved: kj cannot tell who works this clone — declare it with kj identity set`);
|
|
476
|
+
await savePlan(projectDir, plan);
|
|
477
|
+
// KJC-TSK-1065: the host session starts the card here; it learns which skills apply and where to read them.
|
|
478
|
+
if (starting && !flags.json) await printStartSkills({ projectDir, hu, label, deps });
|
|
479
|
+
return emit({ id: hu.id, status, claimed_by: hu.claimed_by ?? null }, `✓ ${label} → ${status}`);
|
|
480
|
+
}
|
|
418
481
|
|
|
419
|
-
|
|
482
|
+
async function printStartSkills({ projectDir, hu, label, deps }) {
|
|
483
|
+
const text = [hu.title, hu.scope, ...(Array.isArray(hu.acceptance_criteria) ? hu.acceptance_criteria : [])].filter(Boolean).join(" ");
|
|
484
|
+
const { skills } = await (deps.skills ?? roleSkills)({ role: "coder", task: text, projectDir });
|
|
485
|
+
const where = (s) => (s.path ? ": read " + s.path : " (native)");
|
|
486
|
+
if (skills.length) console.log([`skills for ${label} (read them before you start):`, ...skills.map((s) => "- " + s.name + where(s))].join("\n"));
|
|
420
487
|
}
|