@opengsd/gsd-core 1.12.0 → 1.13.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-executor.md +63 -35
- package/agents/gsd-plan-checker.md +76 -57
- package/agents/gsd-planner.md +14 -0
- package/agents/gsd-ui-checker.md +19 -3
- package/agents/gsd-ui-researcher.md +29 -0
- package/agents/gsd-verifier.md +23 -1
- package/bin/install.js +239 -67
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +195 -50
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-registry.cjs +514 -114
- package/gsd-core/bin/lib/capability-state.cjs +7 -1
- package/gsd-core/bin/lib/capability-validator.cjs +120 -4
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +85 -2
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/commands.cjs +337 -13
- package/gsd-core/bin/lib/config-loader.cjs +3 -0
- package/gsd-core/bin/lib/core-utils.cjs +34 -7
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +14 -1
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +137 -23
- package/gsd-core/bin/lib/gap-checker.cjs +22 -13
- package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/host-integration.cjs +57 -5
- package/gsd-core/bin/lib/init-command-router.cjs +14 -0
- package/gsd-core/bin/lib/init.cjs +132 -15
- package/gsd-core/bin/lib/install-engine.cjs +184 -12
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
- package/gsd-core/bin/lib/install-profiles.cjs +22 -14
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/io.cjs +35 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +123 -0
- package/gsd-core/bin/lib/milestone.cjs +22 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-id.cjs +251 -9
- package/gsd-core/bin/lib/phase.cjs +774 -35
- package/gsd-core/bin/lib/plan-document.cjs +10 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
- package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-parser.cjs +499 -26
- package/gsd-core/bin/lib/roadmap.cjs +187 -58
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
- package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
- package/gsd-core/bin/lib/smart-entry.cjs +7 -9
- package/gsd-core/bin/lib/state-document.cjs +30 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
- package/gsd-core/bin/lib/state-transition.cjs +333 -44
- package/gsd-core/bin/lib/state.cjs +684 -125
- package/gsd-core/bin/lib/surface.cjs +23 -8
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/uat.cjs +1419 -515
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +273 -12
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +346 -16
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +3 -3
- package/gsd-core/references/edge-probe.md +17 -13
- package/gsd-core/references/execute-mvp-tdd.md +18 -16
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +75 -2
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/tdd.md +14 -9
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/templates/phase-prompt.md +4 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/add-backlog.md +2 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +2 -0
- package/gsd-core/workflows/audit-uat.md +2 -0
- package/gsd-core/workflows/autonomous.md +2 -0
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
- package/gsd-core/workflows/code-review-fix.md +2 -0
- package/gsd-core/workflows/code-review.md +73 -31
- package/gsd-core/workflows/complete-milestone.md +13 -4
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +43 -13
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +2 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +32 -14
- package/gsd-core/workflows/execute-plan.md +8 -8
- package/gsd-core/workflows/explore.md +2 -0
- package/gsd-core/workflows/extract-learnings.md +2 -0
- package/gsd-core/workflows/fast.md +6 -0
- package/gsd-core/workflows/forensics.md +2 -0
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +3 -3
- package/gsd-core/workflows/inbox.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +2 -0
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +2 -0
- package/gsd-core/workflows/list-workspaces.md +2 -0
- package/gsd-core/workflows/manager.md +3 -3
- package/gsd-core/workflows/map-codebase.md +2 -0
- package/gsd-core/workflows/milestone-summary.md +2 -0
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +1 -1
- package/gsd-core/workflows/new-project.md +5 -3
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -0
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +19 -4
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
- package/gsd-core/workflows/plan-phase.md +130 -12
- package/gsd-core/workflows/plan-review-convergence.md +102 -10
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +11 -3
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +25 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
- package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +13 -3
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +2 -0
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +6 -2
- package/gsd-core/workflows/review.md +215 -10
- package/gsd-core/workflows/scan.md +2 -0
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings-integrations.md +9 -8
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +10 -10
- package/gsd-core/workflows/sketch-wrap-up.md +2 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/spec-phase.md +24 -19
- package/gsd-core/workflows/spike-wrap-up.md +2 -0
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +2 -0
- package/gsd-core/workflows/sync-skills.md +12 -4
- package/gsd-core/workflows/thread.md +2 -0
- package/gsd-core/workflows/transition.md +2 -0
- package/gsd-core/workflows/ui-phase.md +26 -5
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +2 -0
- package/gsd-core/workflows/undo.md +1 -1
- package/gsd-core/workflows/update.md +41 -38
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +49 -3
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-context-monitor.js +283 -12
- package/hooks/dist/gsd-node-runner.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +30 -5
- package/hooks/dist/gsd-read-guard.js +2 -0
- package/hooks/dist/gsd-read-injection-scanner.js +5 -5
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-statusline.js +7 -3
- package/hooks/dist/gsd-validate-commit.sh +444 -7
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-context-monitor.js +283 -12
- package/hooks/gsd-node-runner.sh +1 -0
- package/hooks/gsd-prompt-guard.js +30 -5
- package/hooks/gsd-read-guard.js +2 -0
- package/hooks/gsd-read-injection-scanner.js +5 -5
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-statusline.js +7 -3
- package/hooks/gsd-validate-commit.sh +444 -7
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/hooks.json +6 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +5 -5
- package/scripts/build-hooks.js +11 -4
- package/scripts/ci-test-scope.cjs +7 -0
- package/scripts/docs-guard-registry.cjs +10 -0
- package/scripts/gen-loop-host-contract.cjs +67 -15
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +5 -0
- package/scripts/lint-phase-enumeration-drift.cjs +24 -6
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-test-file-count.allowlist.json +3 -1
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Quick-Batch command router — CLI subcommand dispatcher for
|
|
4
|
+
* `gsd-tools quick-batch` (#3676, Phase 4 of epic #3344 / ADR-1239).
|
|
5
|
+
*
|
|
6
|
+
* Quick-batch is a first-party, always-on command family (like `/gsd:quick`,
|
|
7
|
+
* which has no capability-registry entry), so this router is wired directly
|
|
8
|
+
* into `HOST_COMMAND_ROUTERS` (`gsd-core/bin/gsd-tools.cjs`) — NOT the opt-in
|
|
9
|
+
* capability-registry/`activationKey` path `graphify` uses. Shape follows
|
|
10
|
+
* `graphify-command-router.cts` (thin `routeHubCommandFamily` wrapper), which
|
|
11
|
+
* gives the Hub's `makeUnknownCommand` handling for free on an unknown
|
|
12
|
+
* subcommand (test-matrix row 47).
|
|
13
|
+
*
|
|
14
|
+
* Verbs wrap `src/quick-batch.cts` (durable manifest read/write — reused
|
|
15
|
+
* as-is) and `src/quick-batch-dispatch.cts` (pure decision logic, #3676's
|
|
16
|
+
* own new module). This router performs NO decision logic of its own beyond
|
|
17
|
+
* argument shaping — every behavioral rule lives in one of those two
|
|
18
|
+
* modules, per the design doc's "Do the simplest thing" law.
|
|
19
|
+
*
|
|
20
|
+
* Test seam: pass `_quickBatch`/`_quickBatchDispatch` in the options object
|
|
21
|
+
* to inject recording mocks instead of the real modules — same `_`-prefix
|
|
22
|
+
* convention `graphify-command-router.cts` uses.
|
|
23
|
+
*
|
|
24
|
+
* ADR-457 build-at-publish: compiled by tsc to
|
|
25
|
+
* gsd-core/bin/lib/quick-batch-command-router.cjs.
|
|
26
|
+
*/
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const quickBatch = require("./quick-batch.cjs");
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
|
+
const quickBatchDispatch = require("./quick-batch-dispatch.cjs");
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
+
const io = require("./io.cjs");
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const commandRoutingHub = require("./command-routing-hub.cjs");
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
|
+
const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
|
|
37
|
+
const security_cjs_1 = require("./security.cjs");
|
|
38
|
+
const { output, ERROR_REASON } = io;
|
|
39
|
+
const { makeInvalidArgs } = commandRoutingHub;
|
|
40
|
+
const { routeHubCommandFamily } = cjsCommandRouterAdapter;
|
|
41
|
+
// ─── Small arg-parsing helpers (local — no new shared convention needed) ────
|
|
42
|
+
/** `--flag value` lookup; undefined when the flag is absent. */
|
|
43
|
+
function argValue(args, flag) {
|
|
44
|
+
const idx = args.indexOf(flag);
|
|
45
|
+
if (idx === -1)
|
|
46
|
+
return undefined;
|
|
47
|
+
return args[idx + 1];
|
|
48
|
+
}
|
|
49
|
+
function parseJsonArg(raw, label) {
|
|
50
|
+
if (raw === undefined) {
|
|
51
|
+
return { ok: false, reason: `${label} requires a JSON value` };
|
|
52
|
+
}
|
|
53
|
+
const parsed = (0, security_cjs_1.safeJsonParse)(raw, { maxLength: 1048576, label });
|
|
54
|
+
if (!parsed.ok) {
|
|
55
|
+
return { ok: false, reason: `${label} is not valid JSON: ${parsed.error ?? 'unknown parse error'}` };
|
|
56
|
+
}
|
|
57
|
+
return { ok: true, value: parsed.value };
|
|
58
|
+
}
|
|
59
|
+
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
60
|
+
function routeQuickBatchCommand({ args, cwd, raw, error, _quickBatch, _quickBatchDispatch }) {
|
|
61
|
+
const qb = _quickBatch ?? quickBatch;
|
|
62
|
+
const dispatch = _quickBatchDispatch ?? quickBatchDispatch;
|
|
63
|
+
/** Forward a `Result<T>` from either module straight to output()/error(). */
|
|
64
|
+
function emit(result) {
|
|
65
|
+
if (result && typeof result === 'object' && 'ok' in result) {
|
|
66
|
+
const r = result;
|
|
67
|
+
if (!r.ok) {
|
|
68
|
+
error(r.reason ?? 'quick-batch command failed', ERROR_REASON.USAGE);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
output(r.value, raw);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
output(result, raw);
|
|
75
|
+
}
|
|
76
|
+
routeHubCommandFamily({
|
|
77
|
+
family: 'quick-batch',
|
|
78
|
+
args,
|
|
79
|
+
subcommands: [
|
|
80
|
+
'create',
|
|
81
|
+
'update',
|
|
82
|
+
'resume',
|
|
83
|
+
'complete',
|
|
84
|
+
'effective-concurrency',
|
|
85
|
+
'merge-eligible',
|
|
86
|
+
'spawn-plan',
|
|
87
|
+
'filter-executed',
|
|
88
|
+
'verification-routing',
|
|
89
|
+
'merge-routing',
|
|
90
|
+
'cleanup-entry',
|
|
91
|
+
'parse-args',
|
|
92
|
+
],
|
|
93
|
+
handlers: {
|
|
94
|
+
// `quick-batch create --file <path> [--base-revision <sha>] [--options <json>]`
|
|
95
|
+
create: () => {
|
|
96
|
+
const filePath = argValue(args, '--file');
|
|
97
|
+
if (!filePath) {
|
|
98
|
+
return makeInvalidArgs('--file', 'Usage: gsd-tools quick-batch create --file <path> [--base-revision <sha>] [--options <json>]', ERROR_REASON.USAGE);
|
|
99
|
+
}
|
|
100
|
+
const parsed = qb.parseTaskListFromFile(cwd, filePath);
|
|
101
|
+
if (!parsed.ok) {
|
|
102
|
+
return makeInvalidArgs('--file', parsed.reason ?? 'unable to parse task list', ERROR_REASON.USAGE);
|
|
103
|
+
}
|
|
104
|
+
const baseRevision = argValue(args, '--base-revision');
|
|
105
|
+
const optionsRaw = argValue(args, '--options');
|
|
106
|
+
let batchOptions;
|
|
107
|
+
if (optionsRaw !== undefined) {
|
|
108
|
+
const optResult = parseJsonArg(optionsRaw, '--options');
|
|
109
|
+
if (!optResult.ok)
|
|
110
|
+
return makeInvalidArgs('--options', optResult.reason, ERROR_REASON.USAGE);
|
|
111
|
+
batchOptions = optResult.value;
|
|
112
|
+
}
|
|
113
|
+
const items = (parsed.value ?? []).map((it) => ({ description: it.description }));
|
|
114
|
+
emit(qb.createBatch(cwd, items, { baseRevision, batchOptions }));
|
|
115
|
+
},
|
|
116
|
+
// `quick-batch update --batch <id> --updates <json>`
|
|
117
|
+
update: () => {
|
|
118
|
+
const batchId = argValue(args, '--batch');
|
|
119
|
+
if (!batchId) {
|
|
120
|
+
return makeInvalidArgs('--batch', 'Usage: gsd-tools quick-batch update --batch <id> --updates <json>', ERROR_REASON.USAGE);
|
|
121
|
+
}
|
|
122
|
+
const updatesResult = parseJsonArg(argValue(args, '--updates'), '--updates');
|
|
123
|
+
if (!updatesResult.ok)
|
|
124
|
+
return makeInvalidArgs('--updates', updatesResult.reason, ERROR_REASON.USAGE);
|
|
125
|
+
emit(qb.updateBatchItems(cwd, batchId, updatesResult.value));
|
|
126
|
+
},
|
|
127
|
+
// `quick-batch resume --batch <id> [--current-base-revision <sha>]`
|
|
128
|
+
resume: () => {
|
|
129
|
+
const batchId = argValue(args, '--batch');
|
|
130
|
+
if (!batchId) {
|
|
131
|
+
return makeInvalidArgs('--batch', 'Usage: gsd-tools quick-batch resume --batch <id> [--current-base-revision <sha>]', ERROR_REASON.USAGE);
|
|
132
|
+
}
|
|
133
|
+
const currentBaseRevision = argValue(args, '--current-base-revision');
|
|
134
|
+
emit(qb.resumeBatch(cwd, batchId, currentBaseRevision !== undefined ? { currentBaseRevision } : {}));
|
|
135
|
+
},
|
|
136
|
+
// `quick-batch complete --batch <id> --quick-id <id> --description <t> --date <d> --commit <sha> [--directory <dir>]`
|
|
137
|
+
complete: () => {
|
|
138
|
+
const batchId = argValue(args, '--batch');
|
|
139
|
+
const quickId = argValue(args, '--quick-id');
|
|
140
|
+
const description = argValue(args, '--description');
|
|
141
|
+
const date = argValue(args, '--date');
|
|
142
|
+
const commit = argValue(args, '--commit');
|
|
143
|
+
if (!batchId || !quickId || !description || !date || !commit) {
|
|
144
|
+
return makeInvalidArgs('--batch/--quick-id/--description/--date/--commit', 'Usage: gsd-tools quick-batch complete --batch <id> --quick-id <id> --description <t> --date <d> --commit <sha> [--directory <dir>]', ERROR_REASON.USAGE);
|
|
145
|
+
}
|
|
146
|
+
const directory = argValue(args, '--directory');
|
|
147
|
+
emit(qb.completeQuickItem(cwd, batchId, quickId, { description, date, commit, directory }));
|
|
148
|
+
},
|
|
149
|
+
// `quick-batch effective-concurrency --jobs <auto|N> --task-count <N> --capacity <N> --isolation <str> [--mutating]`
|
|
150
|
+
'effective-concurrency': () => {
|
|
151
|
+
const jobsRaw = argValue(args, '--jobs');
|
|
152
|
+
const taskCount = Number(argValue(args, '--task-count'));
|
|
153
|
+
const capacity = Number(argValue(args, '--capacity'));
|
|
154
|
+
const isolation = argValue(args, '--isolation') ?? '';
|
|
155
|
+
if (jobsRaw === undefined || !Number.isFinite(taskCount) || !Number.isFinite(capacity)) {
|
|
156
|
+
return makeInvalidArgs('--jobs/--task-count/--capacity', 'Usage: gsd-tools quick-batch effective-concurrency --jobs <auto|N> --task-count <N> --capacity <N> --isolation <str> [--mutating]', ERROR_REASON.USAGE);
|
|
157
|
+
}
|
|
158
|
+
const jobs = jobsRaw === 'auto' ? 'auto' : Number(jobsRaw);
|
|
159
|
+
const mutating = args.includes('--mutating');
|
|
160
|
+
output({
|
|
161
|
+
concurrency: dispatch.computeEffectiveConcurrency({ jobs, taskCount, capacity, isolation, mutating }),
|
|
162
|
+
}, raw);
|
|
163
|
+
},
|
|
164
|
+
// `quick-batch merge-eligible --wave-order <json-array> --ready <json-array>`
|
|
165
|
+
'merge-eligible': () => {
|
|
166
|
+
const waveOrderResult = parseJsonArg(argValue(args, '--wave-order'), '--wave-order');
|
|
167
|
+
if (!waveOrderResult.ok)
|
|
168
|
+
return makeInvalidArgs('--wave-order', waveOrderResult.reason, ERROR_REASON.USAGE);
|
|
169
|
+
const readyResult = parseJsonArg(argValue(args, '--ready'), '--ready');
|
|
170
|
+
if (!readyResult.ok)
|
|
171
|
+
return makeInvalidArgs('--ready', readyResult.reason, ERROR_REASON.USAGE);
|
|
172
|
+
output({
|
|
173
|
+
mergeable: dispatch.computeMergeOrder(waveOrderResult.value, new Set(readyResult.value)),
|
|
174
|
+
}, raw);
|
|
175
|
+
},
|
|
176
|
+
// `quick-batch spawn-plan --eligible <json-array> --capacity <N> --in-flight <N> [--refused <json-array>]`
|
|
177
|
+
'spawn-plan': () => {
|
|
178
|
+
const eligibleResult = parseJsonArg(argValue(args, '--eligible'), '--eligible');
|
|
179
|
+
if (!eligibleResult.ok)
|
|
180
|
+
return makeInvalidArgs('--eligible', eligibleResult.reason, ERROR_REASON.USAGE);
|
|
181
|
+
const capacity = Number(argValue(args, '--capacity'));
|
|
182
|
+
const currentInFlight = Number(argValue(args, '--in-flight'));
|
|
183
|
+
if (!Number.isFinite(capacity) || !Number.isFinite(currentInFlight)) {
|
|
184
|
+
return makeInvalidArgs('--capacity/--in-flight', 'Usage: gsd-tools quick-batch spawn-plan --eligible <json-array> --capacity <N> --in-flight <N> [--refused <json-array>]', ERROR_REASON.USAGE);
|
|
185
|
+
}
|
|
186
|
+
let refused = [];
|
|
187
|
+
const refusedRaw = argValue(args, '--refused');
|
|
188
|
+
if (refusedRaw !== undefined) {
|
|
189
|
+
const refusedResult = parseJsonArg(refusedRaw, '--refused');
|
|
190
|
+
if (!refusedResult.ok)
|
|
191
|
+
return makeInvalidArgs('--refused', refusedResult.reason, ERROR_REASON.USAGE);
|
|
192
|
+
refused = refusedResult.value;
|
|
193
|
+
}
|
|
194
|
+
output(dispatch.computeSpawnPlan({ eligibleIds: eligibleResult.value, capacity, currentInFlight, refused }), raw);
|
|
195
|
+
},
|
|
196
|
+
// `quick-batch filter-executed --eligible <json-array> --executed <json-array>`
|
|
197
|
+
// Crash-window duplicate-dispatch guard (#3677): splits this round's
|
|
198
|
+
// eligible ids into `spawnEligible` (safe to dispatch) and
|
|
199
|
+
// `alreadyExecuted` (SUMMARY.md already exists on disk — caller
|
|
200
|
+
// determines this via its own filesystem check; NEVER re-dispatch
|
|
201
|
+
// these — merge-wave.md's own on-disk criterion picks them up).
|
|
202
|
+
'filter-executed': () => {
|
|
203
|
+
const eligibleResult = parseJsonArg(argValue(args, '--eligible'), '--eligible');
|
|
204
|
+
if (!eligibleResult.ok)
|
|
205
|
+
return makeInvalidArgs('--eligible', eligibleResult.reason, ERROR_REASON.USAGE);
|
|
206
|
+
const executedResult = parseJsonArg(argValue(args, '--executed'), '--executed');
|
|
207
|
+
if (!executedResult.ok)
|
|
208
|
+
return makeInvalidArgs('--executed', executedResult.reason, ERROR_REASON.USAGE);
|
|
209
|
+
output(dispatch.filterAlreadyExecuted(eligibleResult.value, executedResult.value), raw);
|
|
210
|
+
},
|
|
211
|
+
// `quick-batch verification-routing --status <passed|gaps_found|human_needed>`
|
|
212
|
+
'verification-routing': () => {
|
|
213
|
+
const status = argValue(args, '--status');
|
|
214
|
+
if (status !== 'passed' && status !== 'gaps_found' && status !== 'human_needed') {
|
|
215
|
+
return makeInvalidArgs('--status', 'Usage: gsd-tools quick-batch verification-routing --status <passed|gaps_found|human_needed>', ERROR_REASON.USAGE);
|
|
216
|
+
}
|
|
217
|
+
output(dispatch.routeVerificationOutcome(status), raw);
|
|
218
|
+
},
|
|
219
|
+
// `quick-batch merge-routing --kind <merged|merge_failed|scope_violation> [--detail <text>]`
|
|
220
|
+
'merge-routing': () => {
|
|
221
|
+
const kind = argValue(args, '--kind');
|
|
222
|
+
if (kind !== 'merged' && kind !== 'merge_failed' && kind !== 'scope_violation') {
|
|
223
|
+
return makeInvalidArgs('--kind', 'Usage: gsd-tools quick-batch merge-routing --kind <merged|merge_failed|scope_violation> [--detail <text>]', ERROR_REASON.USAGE);
|
|
224
|
+
}
|
|
225
|
+
const detail = argValue(args, '--detail');
|
|
226
|
+
output(dispatch.routeMergeOutcome(detail !== undefined ? { kind, detail } : { kind }), raw);
|
|
227
|
+
},
|
|
228
|
+
// `quick-batch cleanup-entry --agent-id <id|null> --worktree-path <p> --branch <b> --expected-base <b> [--allowed-bases <json-array>] --plan-content <text>`
|
|
229
|
+
'cleanup-entry': () => {
|
|
230
|
+
const agentIdRaw = argValue(args, '--agent-id');
|
|
231
|
+
const worktreePath = argValue(args, '--worktree-path');
|
|
232
|
+
const branch = argValue(args, '--branch');
|
|
233
|
+
const expectedBase = argValue(args, '--expected-base');
|
|
234
|
+
const planContent = argValue(args, '--plan-content');
|
|
235
|
+
if (!worktreePath || !branch || !expectedBase || planContent === undefined) {
|
|
236
|
+
return makeInvalidArgs('--worktree-path/--branch/--expected-base/--plan-content', 'Usage: gsd-tools quick-batch cleanup-entry --worktree-path <p> --branch <b> --expected-base <b> --plan-content <text> [--agent-id <id>] [--allowed-bases <json-array>]', ERROR_REASON.USAGE);
|
|
237
|
+
}
|
|
238
|
+
let allowedBases;
|
|
239
|
+
const allowedBasesRaw = argValue(args, '--allowed-bases');
|
|
240
|
+
if (allowedBasesRaw !== undefined) {
|
|
241
|
+
const allowedResult = parseJsonArg(allowedBasesRaw, '--allowed-bases');
|
|
242
|
+
if (!allowedResult.ok)
|
|
243
|
+
return makeInvalidArgs('--allowed-bases', allowedResult.reason, ERROR_REASON.USAGE);
|
|
244
|
+
allowedBases = allowedResult.value;
|
|
245
|
+
}
|
|
246
|
+
output(dispatch.buildCleanupManifestEntry({
|
|
247
|
+
agentId: agentIdRaw ?? null,
|
|
248
|
+
worktreePath,
|
|
249
|
+
branch,
|
|
250
|
+
expectedBase,
|
|
251
|
+
allowedBases,
|
|
252
|
+
planContent,
|
|
253
|
+
}), raw);
|
|
254
|
+
},
|
|
255
|
+
// `quick-batch parse-args --text "<raw $ARGUMENTS string>"` (preferred —
|
|
256
|
+
// callers pass the ENTIRE, still-quoted $ARGUMENTS as ONE argv element;
|
|
257
|
+
// this handler does the whitespace split itself, in Node, so shell
|
|
258
|
+
// pathname expansion (globbing) on attacker-influenced task text never
|
|
259
|
+
// happens before this parser sees it — quoting `"$ARGUMENTS"` at the
|
|
260
|
+
// call site is what closes that off; splitting it here is what keeps
|
|
261
|
+
// the caller from having to word-split it unsafely beforehand).
|
|
262
|
+
// `quick-batch parse-args -- <already-tokenized args>` (legacy/direct
|
|
263
|
+
// form — still supported for a caller that already has a real argv
|
|
264
|
+
// array with no shell splitting involved, e.g. a test harness).
|
|
265
|
+
'parse-args': () => {
|
|
266
|
+
const textArg = argValue(args, '--text');
|
|
267
|
+
if (textArg !== undefined) {
|
|
268
|
+
const rawArgs = textArg.trim().length === 0 ? [] : textArg.trim().split(/\s+/);
|
|
269
|
+
emit(dispatch.parseQuickBatchArgs(rawArgs));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const sepIdx = args.indexOf('--');
|
|
273
|
+
const rawArgs = sepIdx === -1 ? [] : args.slice(sepIdx + 1);
|
|
274
|
+
emit(dispatch.parseQuickBatchArgs(rawArgs));
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
unknownMessage: (subcommand, available) => `Unknown quick-batch subcommand. Available: ${available.join(', ')}`,
|
|
278
|
+
error,
|
|
279
|
+
cwd,
|
|
280
|
+
raw,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
module.exports = {
|
|
284
|
+
routeQuickBatchCommand,
|
|
285
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Quick-Batch Dispatch Core (#3676, Phase 4 of epic #3344 / ADR-1239
|
|
4
|
+
* "Quick-batch binding").
|
|
5
|
+
*
|
|
6
|
+
* PURE decision logic for `/gsd:quick-batch`: argument validation, effective
|
|
7
|
+
* concurrency, deterministic merge ordering, spawn backpressure, and
|
|
8
|
+
* failure/verification routing. This module answers "what should happen
|
|
9
|
+
* next" — it NEVER performs `Agent()` dispatch, `git worktree` I/O, or writes
|
|
10
|
+
* `BATCH.json`/STATE.md itself. Those live in the workflow markdown (a
|
|
11
|
+
* separate follow-up pass) and in `src/quick-batch.cts` (durable manifest
|
|
12
|
+
* mutation, reused as-is).
|
|
13
|
+
*
|
|
14
|
+
* Capacity and isolation are CALLER-SUPPLIED inputs here — the CLI layer
|
|
15
|
+
* resolves those via the existing `dispatch-capacity`/`dispatch-isolation`
|
|
16
|
+
* queries (`gsd-core/bin/gsd-tools.cjs`'s `routeDispatchCapacity`/
|
|
17
|
+
* `routeDispatchIsolation`); this module never re-derives that negotiation.
|
|
18
|
+
*
|
|
19
|
+
* The one exception to "never performs I/O" is `buildCleanupManifestEntry`,
|
|
20
|
+
* which accepts a plan document's raw text (already read by the caller) and
|
|
21
|
+
* parses it via the existing `parsePlanDocument` (`src/plan-document.cts`) —
|
|
22
|
+
* no filesystem access happens inside this module.
|
|
23
|
+
*
|
|
24
|
+
* Design lock: `.gsd/phase/feat-3676-quick-batch-command-workflow/40-design.md`.
|
|
25
|
+
* Test matrix: `.gsd/phase/feat-3676-quick-batch-command-workflow/50-test-matrix.md`.
|
|
26
|
+
*
|
|
27
|
+
* ADR-457 build-at-publish: compiled by tsc to gsd-core/bin/lib/quick-batch-dispatch.cjs.
|
|
28
|
+
*/
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
|
+
const planDocumentMod = require("./plan-document.cjs");
|
|
31
|
+
const { parsePlanDocument } = planDocumentMod;
|
|
32
|
+
/**
|
|
33
|
+
* Validate `/gsd:quick-batch` CLI args BEFORE any dispatch. Rejects
|
|
34
|
+
* `--discuss`/`--full` outright (v1 exclusion, design rows 7-9,13-15 — a
|
|
35
|
+
* mixed valid+rejected-flag invocation is still rejected: presence alone is
|
|
36
|
+
* sufficient, regardless of other flags), and rejects a malformed `--jobs`
|
|
37
|
+
* value (non-numeric or <= 0, row 5/9) before any dispatch. `--jobs` omitted
|
|
38
|
+
* defaults to `'auto'` (row 10).
|
|
39
|
+
*/
|
|
40
|
+
function parseQuickBatchArgs(args) {
|
|
41
|
+
if (!Array.isArray(args)) {
|
|
42
|
+
return { ok: false, reason: 'quick-batch args must be an array' };
|
|
43
|
+
}
|
|
44
|
+
if (args.includes('--discuss')) {
|
|
45
|
+
return { ok: false, reason: 'quick-batch does not support --discuss in v1 — use /gsd:quick --discuss per item instead' };
|
|
46
|
+
}
|
|
47
|
+
if (args.includes('--full')) {
|
|
48
|
+
return { ok: false, reason: 'quick-batch does not support --full in v1' };
|
|
49
|
+
}
|
|
50
|
+
let jobs = 'auto';
|
|
51
|
+
const jobsIdx = args.indexOf('--jobs');
|
|
52
|
+
if (jobsIdx !== -1) {
|
|
53
|
+
const raw = args[jobsIdx + 1];
|
|
54
|
+
if (raw === undefined || raw.startsWith('--')) {
|
|
55
|
+
return { ok: false, reason: 'quick-batch --jobs requires a value ("auto" or a positive integer)' };
|
|
56
|
+
}
|
|
57
|
+
if (raw === 'auto') {
|
|
58
|
+
jobs = 'auto';
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const n = Number(raw);
|
|
62
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
63
|
+
return { ok: false, reason: `quick-batch --jobs must be "auto" or a positive integer, got ${JSON.stringify(raw)}` };
|
|
64
|
+
}
|
|
65
|
+
jobs = n;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const validate = args.includes('--validate');
|
|
69
|
+
const research = args.includes('--research');
|
|
70
|
+
let resume = null;
|
|
71
|
+
const resumeIdx = args.indexOf('--resume');
|
|
72
|
+
if (resumeIdx !== -1) {
|
|
73
|
+
const raw = args[resumeIdx + 1];
|
|
74
|
+
if (raw === undefined || raw.startsWith('--')) {
|
|
75
|
+
return { ok: false, reason: 'quick-batch --resume requires a batch id' };
|
|
76
|
+
}
|
|
77
|
+
resume = raw;
|
|
78
|
+
}
|
|
79
|
+
return { ok: true, value: { jobs, validate, research, resume } };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* `--jobs auto` (or omitted) → capacity alone (rows 3,10). `--jobs N` →
|
|
83
|
+
* `min(taskCount, N, capacity)` (row 4). When `isolation === 'none'` and the
|
|
84
|
+
* wave is `mutating`, concurrency is forced to 1 regardless of `--jobs`/
|
|
85
|
+
* capacity (row 6) — a non-mutating wave is unaffected (row 12).
|
|
86
|
+
*/
|
|
87
|
+
function computeEffectiveConcurrency(input) {
|
|
88
|
+
let concurrency = input.jobs === 'auto'
|
|
89
|
+
? input.capacity
|
|
90
|
+
: Math.min(input.taskCount, input.jobs, input.capacity);
|
|
91
|
+
if (input.isolation === 'none' && input.mutating) {
|
|
92
|
+
concurrency = Math.min(concurrency, 1);
|
|
93
|
+
}
|
|
94
|
+
return Math.max(concurrency, 0);
|
|
95
|
+
}
|
|
96
|
+
// ─── Deterministic merge ordering (design row 24; test-matrix rows 32-33) ───
|
|
97
|
+
/**
|
|
98
|
+
* Given a wave's ORIGINAL dispatch order (`waveOrder`, as `computeWaves`
|
|
99
|
+
* assigned it) and the set of ids whose leaf has finished ("ready"),
|
|
100
|
+
* returns the prefix of `waveOrder` that is currently mergeable — merges
|
|
101
|
+
* happen strictly in wave order, so an item is only mergeable once every
|
|
102
|
+
* item before it in `waveOrder` is ALSO ready. An out-of-order completion
|
|
103
|
+
* (e.g. item 2 finishes before item 1) waits: `computeMergeOrder` returns
|
|
104
|
+
* only `[]` until item 1 is also ready.
|
|
105
|
+
*/
|
|
106
|
+
function computeMergeOrder(waveOrder, readyIds) {
|
|
107
|
+
const mergeable = [];
|
|
108
|
+
for (const id of waveOrder) {
|
|
109
|
+
if (!readyIds.has(id))
|
|
110
|
+
break;
|
|
111
|
+
mergeable.push(id);
|
|
112
|
+
}
|
|
113
|
+
return mergeable;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Pure backpressure model: given eligible items, a capacity ceiling, and a
|
|
117
|
+
* set of host-refused ids, decide which ids spawn now and which stay
|
|
118
|
+
* `pending`. Total in-flight (`currentInFlight + spawn.length`) never
|
|
119
|
+
* exceeds `capacity` at any point (row 27, property row 53). A refused id
|
|
120
|
+
* is never counted against capacity and never marked `failed` — it simply
|
|
121
|
+
* returns to `pending` for a later round.
|
|
122
|
+
*/
|
|
123
|
+
function computeSpawnPlan(input) {
|
|
124
|
+
const refusedSet = input.refused instanceof Set ? input.refused : new Set(input.refused ?? []);
|
|
125
|
+
let available = Math.max(input.capacity - input.currentInFlight, 0);
|
|
126
|
+
const spawn = [];
|
|
127
|
+
const pending = [];
|
|
128
|
+
for (const id of input.eligibleIds) {
|
|
129
|
+
if (refusedSet.has(id)) {
|
|
130
|
+
pending.push(id);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (available > 0) {
|
|
134
|
+
spawn.push(id);
|
|
135
|
+
available -= 1;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
pending.push(id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { spawn, pending };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Route a verifier's status to an item action. `human_needed` is terminal
|
|
145
|
+
* for the item — the caller must NOT call `completeQuickItem` (row 30, no
|
|
146
|
+
* STATE row appended). `gaps_found` fails the item WITHOUT rollback and
|
|
147
|
+
* WITHOUT an automatic gap-fix retry (row 31,34). `passed` completes it.
|
|
148
|
+
*/
|
|
149
|
+
function routeVerificationOutcome(status) {
|
|
150
|
+
switch (status) {
|
|
151
|
+
case 'passed':
|
|
152
|
+
return { action: 'complete' };
|
|
153
|
+
case 'human_needed':
|
|
154
|
+
return { action: 'human_needed' };
|
|
155
|
+
case 'gaps_found':
|
|
156
|
+
return { action: 'fail', failureReason: 'verification reported gaps_found (no automatic gap-fix retry in v1)' };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Route a merge attempt's outcome to an item action. Both `merge_failed`
|
|
161
|
+
* (real conflict) and `scope_violation` (undeclared deletion / advisory
|
|
162
|
+
* scope drift escalated to a gate — see `executeWorktreeWaveCleanupPlan`)
|
|
163
|
+
* mark the item `failed` with a `failure_reason` and PRESERVE the worktree
|
|
164
|
+
* for diagnosis — this function never signals worktree removal (row 28,
|
|
165
|
+
* 34-35). `merged` completes the item.
|
|
166
|
+
*/
|
|
167
|
+
function routeMergeOutcome(outcome) {
|
|
168
|
+
switch (outcome.kind) {
|
|
169
|
+
case 'merged':
|
|
170
|
+
return { action: 'complete' };
|
|
171
|
+
case 'merge_failed':
|
|
172
|
+
return {
|
|
173
|
+
action: 'fail',
|
|
174
|
+
failureReason: outcome.detail ? `merge_failed: ${outcome.detail}` : 'merge_failed',
|
|
175
|
+
preserveWorktree: true,
|
|
176
|
+
};
|
|
177
|
+
case 'scope_violation':
|
|
178
|
+
return {
|
|
179
|
+
action: 'fail',
|
|
180
|
+
failureReason: outcome.detail ? `scope_violation: ${outcome.detail}` : 'scope_violation',
|
|
181
|
+
preserveWorktree: true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Crash-window duplicate-dispatch guard
|
|
187
|
+
* (`.gsd/phase/feat-3677-quick-batch-hardening-acceptance/40-design.md` §1).
|
|
188
|
+
* `quick-batch resume`'s `eligible` is purely status/dependency-derived —
|
|
189
|
+
* it has no awareness that an item already finished executing (a real
|
|
190
|
+
* commit, `SUMMARY.md` written) before a coordinator crash left
|
|
191
|
+
* `BATCH.json` at `pending` (the STATE.md-row crash-window detection
|
|
192
|
+
* inside `resumeBatch` only fires once Step 9 has run). This mirrors the
|
|
193
|
+
* file-existence exclusion `planner-wave.md` already applies one layer
|
|
194
|
+
* earlier for `PLAN.md`, extracted as its own pure decision (rather than
|
|
195
|
+
* left as workflow prose only) so it is independently testable: given the
|
|
196
|
+
* eligible ids for this round and the subset the CALLER has already
|
|
197
|
+
* determined finished executing, splits them into ids safe to spawn now
|
|
198
|
+
* and ids that must never be re-dispatched.
|
|
199
|
+
*/
|
|
200
|
+
function filterAlreadyExecuted(eligibleIds, executedIds) {
|
|
201
|
+
const executedSet = executedIds instanceof Set ? executedIds : new Set(executedIds);
|
|
202
|
+
const spawnEligible = [];
|
|
203
|
+
const alreadyExecuted = [];
|
|
204
|
+
for (const id of eligibleIds) {
|
|
205
|
+
if (executedSet.has(id)) {
|
|
206
|
+
alreadyExecuted.push(id);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
spawnEligible.push(id);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { spawnEligible, alreadyExecuted };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Build one `worktree.cleanup-wave` manifest entry for an item, deriving
|
|
216
|
+
* `files_modified`/`declared_deletions` FRESH from the item's own PLAN.md via
|
|
217
|
+
* the existing `parsePlanDocument` (never from `BATCH.json`'s `planned_files`
|
|
218
|
+
* alone — Open Question 2's accepted resolution). An empty `declared_deletions`
|
|
219
|
+
* for a plan that genuinely deletes nothing is indistinguishable from a plan
|
|
220
|
+
* that forgot to declare one — this is `partitionDeclaredDeletions`'s own
|
|
221
|
+
* pre-existing "absent/empty = declares nothing" convention, inherited here,
|
|
222
|
+
* not resolved.
|
|
223
|
+
*/
|
|
224
|
+
function buildCleanupManifestEntry(input) {
|
|
225
|
+
const parsed = parsePlanDocument(input.planContent);
|
|
226
|
+
const entry = {
|
|
227
|
+
agent_id: input.agentId,
|
|
228
|
+
worktree_path: input.worktreePath,
|
|
229
|
+
branch: input.branch,
|
|
230
|
+
expected_base: input.expectedBase,
|
|
231
|
+
files_modified: parsed.filesModified,
|
|
232
|
+
declared_deletions: parsed.filesDeleted,
|
|
233
|
+
};
|
|
234
|
+
if (input.allowedBases !== undefined) {
|
|
235
|
+
entry.allowed_bases = input.allowedBases;
|
|
236
|
+
}
|
|
237
|
+
return entry;
|
|
238
|
+
}
|
|
239
|
+
// ─── Exports ────────────────────────────────────────────────────────────────
|
|
240
|
+
const quickBatchDispatch = {
|
|
241
|
+
parseQuickBatchArgs,
|
|
242
|
+
computeEffectiveConcurrency,
|
|
243
|
+
computeMergeOrder,
|
|
244
|
+
computeSpawnPlan,
|
|
245
|
+
routeVerificationOutcome,
|
|
246
|
+
routeMergeOutcome,
|
|
247
|
+
buildCleanupManifestEntry,
|
|
248
|
+
filterAlreadyExecuted,
|
|
249
|
+
};
|
|
250
|
+
module.exports = quickBatchDispatch;
|