@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,840 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Quick-Batch Core Primitives (#3675, part of epic #3344 / ADR-1239
|
|
4
|
+
* "Quick-batch binding").
|
|
5
|
+
*
|
|
6
|
+
* Pure/state primitives and CLI-testable core operations for batching
|
|
7
|
+
* several `/gsd:quick`-shaped tasks together: task-list parsing, collision-safe
|
|
8
|
+
* quick-ID preallocation, `BATCH.json` schema/validation/resume, deterministic
|
|
9
|
+
* dependency-DAG + file-overlap wave construction, and exactly-once STATE.md
|
|
10
|
+
* completion. NO agent dispatch, NO worktree creation, NO user-facing command —
|
|
11
|
+
* those are Phase 4 (#3676)'s job. This module only ADDS new call sites onto
|
|
12
|
+
* four existing, unmodified primitives:
|
|
13
|
+
*
|
|
14
|
+
* - `cmdInitQuick`'s quick-ID grammar (src/init.cts) — replicated here (NOT
|
|
15
|
+
* delegated to per-item, since that function's own 2-second granularity is
|
|
16
|
+
* not collision-safe under batch allocation; see `allocateQuickIds`).
|
|
17
|
+
* - `appendQuickTaskRow` (src/markdown-table.cts) — reused as-is, called at
|
|
18
|
+
* most once per quick id, gated by our own idempotency check (see
|
|
19
|
+
* `hasQuickTaskRow`) since the function itself carries no idempotency.
|
|
20
|
+
* - `withPlanningLock` (src/planning-workspace.cts) — reused as-is; every
|
|
21
|
+
* durable, cross-call collision-sensitive operation here
|
|
22
|
+
* (`createBatch`/`completeQuickItem`/`resumeBatch`) runs inside exactly
|
|
23
|
+
* ONE (never nested) `withPlanningLock` transaction.
|
|
24
|
+
* - `partitionByFileOverlap` (src/file-overlap-partitioner.cts, #3674) —
|
|
25
|
+
* reused as-is, called per dependency-DAG layer in `computeWaves`, over
|
|
26
|
+
* PRE-NORMALIZED `planned_files` (normalization happens at THIS module's
|
|
27
|
+
* boundary, never inside the Phase 2 helper — its own design lock).
|
|
28
|
+
*
|
|
29
|
+
* `BATCH.json` lives at `.planning/quick-batches/<batch-id>/BATCH.json` — a
|
|
30
|
+
* SIBLING of `.planning/quick/`, never inside it, so `scanQuickTasks`
|
|
31
|
+
* (src/audit.cts) never misreads a batch manifest as a broken quick task.
|
|
32
|
+
*
|
|
33
|
+
* ADR-457 build-at-publish: compiled by tsc to gsd-core/bin/lib/quick-batch.cjs.
|
|
34
|
+
*/
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
39
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
40
|
+
const security_cjs_1 = require("./security.cjs");
|
|
41
|
+
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
42
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
43
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
44
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
|
+
const planningWorkspace = require("./planning-workspace.cjs");
|
|
47
|
+
const { withPlanningLock, planningDir, planningRoot, planningPaths, quickDirFrom } = planningWorkspace;
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
49
|
+
const fileOverlapPartitioner = require("./file-overlap-partitioner.cjs");
|
|
50
|
+
const { partitionByFileOverlap } = fileOverlapPartitioner;
|
|
51
|
+
// ─── Quick-id grammar (replicated from cmdInitQuick, unchanged) ───────────────
|
|
52
|
+
const QUICK_ID_RE = /^\d{6}-[0-9a-z]{3}$/;
|
|
53
|
+
const MAX_TIME_BLOCK = 36 * 36 * 36 - 1; // 3-char base36 ceiling (46655)
|
|
54
|
+
function idFromDateAndBlock(dateStr, block) {
|
|
55
|
+
return dateStr + '-' + block.toString(36).padStart(3, '0');
|
|
56
|
+
}
|
|
57
|
+
/** yyMMdd + starting time-block (floor(secondsSinceMidnight/2)) for `instant`. */
|
|
58
|
+
function dateAndStartBlock(instant) {
|
|
59
|
+
const yy = String(instant.getFullYear()).slice(-2);
|
|
60
|
+
const mm = String(instant.getMonth() + 1).padStart(2, '0');
|
|
61
|
+
const dd = String(instant.getDate()).padStart(2, '0');
|
|
62
|
+
const dateStr = yy + mm + dd;
|
|
63
|
+
const secondsSinceMidnight = instant.getHours() * 3600 + instant.getMinutes() * 60 + instant.getSeconds();
|
|
64
|
+
const startBlock = Math.floor(secondsSinceMidnight / 2);
|
|
65
|
+
return { dateStr, startBlock };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Generate `count` distinct `YYMMDD-xxx` ids starting at `startBlock`,
|
|
69
|
+
* advancing past any id already in `used` (mutated in place with the newly
|
|
70
|
+
* allocated ids too, so a second call sharing the same `used` set never
|
|
71
|
+
* re-issues one of them). Throws if the day's block space is exhausted
|
|
72
|
+
* (46656 ids/day — never reachable in practice, but a real fail-closed ceiling
|
|
73
|
+
* rather than an infinite loop).
|
|
74
|
+
*/
|
|
75
|
+
function allocateIdsGivenUsed(dateStr, startBlock, count, used) {
|
|
76
|
+
const result = [];
|
|
77
|
+
let block = startBlock;
|
|
78
|
+
while (result.length < count) {
|
|
79
|
+
if (block > MAX_TIME_BLOCK) {
|
|
80
|
+
throw new Error(`quick-batch: exhausted collision-free quick ids for ${dateStr} (advanced past block ${MAX_TIME_BLOCK})`);
|
|
81
|
+
}
|
|
82
|
+
const candidate = idFromDateAndBlock(dateStr, block);
|
|
83
|
+
if (!used.has(candidate)) {
|
|
84
|
+
used.add(candidate);
|
|
85
|
+
result.push(candidate);
|
|
86
|
+
}
|
|
87
|
+
block++;
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
/** Existing quick ids already claimed on disk under `.planning/quick/`. */
|
|
92
|
+
function collectExistingQuickIds(quickDir) {
|
|
93
|
+
const usedIds = new Set();
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = node_fs_1.default.readdirSync(quickDir);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return usedIds;
|
|
100
|
+
}
|
|
101
|
+
for (const name of entries) {
|
|
102
|
+
const m = /^(\d{6}-[0-9a-z]{3})(?:-|$)/.exec(name);
|
|
103
|
+
if (m)
|
|
104
|
+
usedIds.add(m[1]);
|
|
105
|
+
}
|
|
106
|
+
return usedIds;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Existing quick ids (and batch ids) already claimed by SIBLING batches under
|
|
110
|
+
* `.planning/quick-batches/*` — mutates `used` in place. This is what makes
|
|
111
|
+
* two concurrent (lock-serialized) `createBatch` calls collision-free even
|
|
112
|
+
* before either batch has dispatched any real `.planning/quick/<id>-<slug>`
|
|
113
|
+
* directory (#3675 design lock row 8/15): the second call's allocation scan
|
|
114
|
+
* sees the first call's already-PERSISTED `BATCH.json`, not just real
|
|
115
|
+
* dispatched directories. A corrupt/unreadable sibling manifest is skipped,
|
|
116
|
+
* never allowed to block a DIFFERENT batch's allocation.
|
|
117
|
+
*/
|
|
118
|
+
function collectExistingBatchQuickIds(cwd, used) {
|
|
119
|
+
const batchesDir = node_path_1.default.join(planningDir(cwd), 'quick-batches');
|
|
120
|
+
let entries;
|
|
121
|
+
try {
|
|
122
|
+
entries = node_fs_1.default.readdirSync(batchesDir);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
for (const name of entries) {
|
|
128
|
+
const manifestPath = node_path_1.default.join(batchesDir, name, 'BATCH.json');
|
|
129
|
+
let raw;
|
|
130
|
+
try {
|
|
131
|
+
raw = node_fs_1.default.readFileSync(manifestPath, 'utf-8');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const parsed = (0, security_cjs_1.safeJsonParse)(raw, { maxLength: 1048576, label: 'sibling BATCH.json' });
|
|
137
|
+
if (!parsed.ok || !parsed.value || typeof parsed.value !== 'object')
|
|
138
|
+
continue;
|
|
139
|
+
const obj = parsed.value;
|
|
140
|
+
if (typeof obj.batch_id === 'string')
|
|
141
|
+
used.add(obj.batch_id);
|
|
142
|
+
if (Array.isArray(obj.items)) {
|
|
143
|
+
for (const it of obj.items) {
|
|
144
|
+
if (it && typeof it === 'object' && typeof it.quick_id === 'string') {
|
|
145
|
+
used.add(it.quick_id);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Allocate `count` distinct, collision-free `YYMMDD-xxx` quick ids under
|
|
153
|
+
* `withPlanningLock`, checked against on-disk `.planning/quick/` entries.
|
|
154
|
+
*
|
|
155
|
+
* NOTE: this primitive does NOT persist anything — it only reserves ids in
|
|
156
|
+
* memory for the duration of one call. Cross-call collision-freedom (#3675
|
|
157
|
+
* design lock row 8/15) is a property of `createBatch`, which allocates AND
|
|
158
|
+
* durably writes `BATCH.json` inside the SAME lock transaction; two bare
|
|
159
|
+
* `allocateQuickIds` calls with no intervening persistence can still overlap.
|
|
160
|
+
* Exposed standalone for direct grammar-level testability (rows 7/13/14).
|
|
161
|
+
*/
|
|
162
|
+
function allocateQuickIds(cwd, count, options = {}) {
|
|
163
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
164
|
+
return { ok: false, reason: `count must be a positive integer, got ${String(count)}` };
|
|
165
|
+
}
|
|
166
|
+
const clock = options.clock ?? clock_cjs_1.realClock;
|
|
167
|
+
try {
|
|
168
|
+
const ids = withPlanningLock(cwd, () => {
|
|
169
|
+
const quickDir = quickDirFrom(planningDir(cwd));
|
|
170
|
+
const used = collectExistingQuickIds(quickDir);
|
|
171
|
+
collectExistingBatchQuickIds(cwd, used);
|
|
172
|
+
const { dateStr, startBlock } = dateAndStartBlock(new Date(clock.now()));
|
|
173
|
+
return allocateIdsGivenUsed(dateStr, startBlock, count, used);
|
|
174
|
+
}, clock);
|
|
175
|
+
return { ok: true, value: ids };
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ─── Task-list parsing ──────────────────────────────────────────────────────────
|
|
182
|
+
const BULLET_OR_NUMBER_RE = /^\s*(?:[-*]|\d+[.)])\s+(.+?)\s*$/;
|
|
183
|
+
/**
|
|
184
|
+
* Parse an inline bulleted (`-`/`*`) or numbered (`1.`/`1)`) task list.
|
|
185
|
+
* Non-matching lines (blank lines, prose) are ignored. Order is preserved;
|
|
186
|
+
* duplicate descriptions are preserved as distinct entries (never deduped).
|
|
187
|
+
* Requires at least 2 items (#3675/#3344 AC minimum for a "batch").
|
|
188
|
+
*/
|
|
189
|
+
function parseTaskList(text) {
|
|
190
|
+
if (typeof text !== 'string') {
|
|
191
|
+
return { ok: false, reason: 'task list input must be a string' };
|
|
192
|
+
}
|
|
193
|
+
const items = [];
|
|
194
|
+
for (const line of text.split(/\r?\n/)) {
|
|
195
|
+
const m = BULLET_OR_NUMBER_RE.exec(line);
|
|
196
|
+
if (m)
|
|
197
|
+
items.push({ description: m[1] });
|
|
198
|
+
}
|
|
199
|
+
if (items.length < 2) {
|
|
200
|
+
return { ok: false, reason: `quick-batch requires at least 2 explicit tasks, found ${items.length}` };
|
|
201
|
+
}
|
|
202
|
+
return { ok: true, value: items };
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Parse a task list from a file, strictly confined to the planning workspace
|
|
206
|
+
* root (`.planning/`) — traversal, symlink escape, and non-regular-file
|
|
207
|
+
* targets (directory, socket, device, FIFO) are all rejected.
|
|
208
|
+
*/
|
|
209
|
+
function parseTaskListFromFile(cwd, filePath) {
|
|
210
|
+
const root = planningRoot(cwd);
|
|
211
|
+
let safePath;
|
|
212
|
+
try {
|
|
213
|
+
safePath = (0, security_cjs_1.requireSafePath)(filePath, root, 'quick-batch --file', { allowAbsolute: true });
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
217
|
+
}
|
|
218
|
+
let stat;
|
|
219
|
+
try {
|
|
220
|
+
stat = node_fs_1.default.statSync(safePath);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
return { ok: false, reason: `unable to stat --file path: ${err instanceof Error ? err.message : String(err)}` };
|
|
224
|
+
}
|
|
225
|
+
if (!stat.isFile()) {
|
|
226
|
+
return { ok: false, reason: `--file path is not a regular file: ${filePath}` };
|
|
227
|
+
}
|
|
228
|
+
let content;
|
|
229
|
+
try {
|
|
230
|
+
content = node_fs_1.default.readFileSync(safePath, 'utf-8');
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
return { ok: false, reason: `unable to read --file path: ${err instanceof Error ? err.message : String(err)}` };
|
|
234
|
+
}
|
|
235
|
+
return parseTaskList(content);
|
|
236
|
+
}
|
|
237
|
+
// ─── Dependency-DAG validation ──────────────────────────────────────────────────
|
|
238
|
+
function resolveDependencyIndex(ref, items) {
|
|
239
|
+
if (typeof ref === 'number') {
|
|
240
|
+
return Number.isInteger(ref) && ref >= 0 && ref < items.length ? ref : null;
|
|
241
|
+
}
|
|
242
|
+
if (typeof ref === 'string') {
|
|
243
|
+
const idx = items.findIndex((it) => it.clientId === ref);
|
|
244
|
+
return idx === -1 ? null : idx;
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Validate that every `dependsOn` reference resolves to another item WITHIN
|
|
250
|
+
* this same input array (never an unknown/cross-batch reference), and that
|
|
251
|
+
* the resulting dependency graph is acyclic. Fails closed before any wave is
|
|
252
|
+
* built, per the #3675 design lock.
|
|
253
|
+
*/
|
|
254
|
+
function validateDag(items) {
|
|
255
|
+
const adj = items.map(() => []);
|
|
256
|
+
for (let i = 0; i < items.length; i++) {
|
|
257
|
+
const deps = items[i].dependsOn ?? [];
|
|
258
|
+
for (const ref of deps) {
|
|
259
|
+
const idx = resolveDependencyIndex(ref, items);
|
|
260
|
+
if (idx === null) {
|
|
261
|
+
return { ok: false, reason: `item ${i} (${items[i].description}) declares an unknown dependency reference: ${JSON.stringify(ref)}` };
|
|
262
|
+
}
|
|
263
|
+
if (idx === i) {
|
|
264
|
+
return { ok: false, reason: `item ${i} (${items[i].description}) declares a dependency on itself` };
|
|
265
|
+
}
|
|
266
|
+
adj[i].push(idx);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
270
|
+
const color = new Array(items.length).fill(WHITE);
|
|
271
|
+
const stack = [];
|
|
272
|
+
let cycleDiagnostic = null;
|
|
273
|
+
const dfs = (u) => {
|
|
274
|
+
color[u] = GRAY;
|
|
275
|
+
stack.push(u);
|
|
276
|
+
for (const v of adj[u]) {
|
|
277
|
+
if (color[v] === GRAY) {
|
|
278
|
+
const idx = stack.indexOf(v);
|
|
279
|
+
cycleDiagnostic = `dependency cycle detected: ${stack.slice(idx).concat(v).join(' -> ')}`;
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (color[v] === WHITE && dfs(v))
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
stack.pop();
|
|
286
|
+
color[u] = BLACK;
|
|
287
|
+
return false;
|
|
288
|
+
};
|
|
289
|
+
for (let i = 0; i < items.length; i++) {
|
|
290
|
+
if (color[i] === WHITE && dfs(i)) {
|
|
291
|
+
return { ok: false, reason: cycleDiagnostic ?? 'dependency cycle detected' };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { ok: true, value: adj };
|
|
295
|
+
}
|
|
296
|
+
/** Reshape persisted `QuickBatchItem`s into `computeWaves`' input shape. */
|
|
297
|
+
function toWaveInput(items) {
|
|
298
|
+
return items.map((it) => ({ quickId: it.quick_id, dependsOn: it.depends_on, plannedFiles: it.planned_files }));
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Deterministic wave construction: an item's wave is strictly after every one
|
|
302
|
+
* of its dependencies' waves (DAG readiness), and no two items in the same
|
|
303
|
+
* wave share a (normalized) file — reusing `partitionByFileOverlap` per
|
|
304
|
+
* dependency-DAG layer, over path-separator-normalized `plannedFiles`
|
|
305
|
+
* (normalization happens HERE, never inside `partitionByFileOverlap` itself,
|
|
306
|
+
* per Phase 2's own design lock). Same input order always yields the same
|
|
307
|
+
* waves. Fails closed on an unknown dependency or a cycle.
|
|
308
|
+
*/
|
|
309
|
+
function computeWaves(items) {
|
|
310
|
+
const byId = new Map(items.map((it) => [it.quickId, it]));
|
|
311
|
+
for (const it of items) {
|
|
312
|
+
for (const dep of it.dependsOn) {
|
|
313
|
+
if (!byId.has(dep)) {
|
|
314
|
+
return { ok: false, reason: `item ${it.quickId} depends on unknown item ${dep}` };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const layer = new Map();
|
|
319
|
+
const state = new Map();
|
|
320
|
+
let cycleDiagnostic = null;
|
|
321
|
+
const visit = (id, pathStack) => {
|
|
322
|
+
const st = state.get(id) ?? 0;
|
|
323
|
+
if (st === 1) {
|
|
324
|
+
const idx = pathStack.indexOf(id);
|
|
325
|
+
cycleDiagnostic = `dependency cycle detected: ${pathStack.slice(idx).concat(id).join(' -> ')}`;
|
|
326
|
+
return -1;
|
|
327
|
+
}
|
|
328
|
+
if (st === 2)
|
|
329
|
+
return layer.get(id);
|
|
330
|
+
state.set(id, 1);
|
|
331
|
+
const it = byId.get(id);
|
|
332
|
+
let maxDepLayer = -1;
|
|
333
|
+
for (const dep of it.dependsOn) {
|
|
334
|
+
const depLayer = visit(dep, [...pathStack, id]);
|
|
335
|
+
if (depLayer === -1)
|
|
336
|
+
return -1;
|
|
337
|
+
if (depLayer > maxDepLayer)
|
|
338
|
+
maxDepLayer = depLayer;
|
|
339
|
+
}
|
|
340
|
+
const l = maxDepLayer + 1;
|
|
341
|
+
layer.set(id, l);
|
|
342
|
+
state.set(id, 2);
|
|
343
|
+
return l;
|
|
344
|
+
};
|
|
345
|
+
for (const it of items) {
|
|
346
|
+
if (visit(it.quickId, []) === -1) {
|
|
347
|
+
return { ok: false, reason: cycleDiagnostic ?? 'dependency cycle detected' };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const maxLayer = items.length ? Math.max(...items.map((it) => layer.get(it.quickId))) : -1;
|
|
351
|
+
const waves = [];
|
|
352
|
+
for (let l = 0; l <= maxLayer; l++) {
|
|
353
|
+
const layerItems = items.filter((it) => layer.get(it.quickId) === l);
|
|
354
|
+
const overlapInput = layerItems.map((it) => ({
|
|
355
|
+
id: it.quickId,
|
|
356
|
+
files: it.plannedFiles.map(shell_command_projection_cjs_1.posixNormalize),
|
|
357
|
+
}));
|
|
358
|
+
const stages = partitionByFileOverlap(overlapInput);
|
|
359
|
+
for (const stage of stages)
|
|
360
|
+
waves.push(stage);
|
|
361
|
+
}
|
|
362
|
+
return { ok: true, value: waves };
|
|
363
|
+
}
|
|
364
|
+
// ─── BATCH.json creation ────────────────────────────────────────────────────────
|
|
365
|
+
function batchManifestPath(cwd, batchId) {
|
|
366
|
+
return node_path_1.default.join(planningDir(cwd), 'quick-batches', batchId, 'BATCH.json');
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Create a new quick-batch: validates the dependency DAG, allocates N+1
|
|
370
|
+
* collision-free quick ids (one for the batch itself, one per item) and
|
|
371
|
+
* durably writes `BATCH.json` — all inside ONE `withPlanningLock` transaction,
|
|
372
|
+
* so concurrent `createBatch` calls can never collide (#3675 design lock).
|
|
373
|
+
* `BATCH.json` lives at `.planning/quick-batches/<batch-id>/BATCH.json`, a
|
|
374
|
+
* SIBLING of `.planning/quick/` — never inside it (scanQuickTasks safety).
|
|
375
|
+
*/
|
|
376
|
+
function createBatch(cwd, itemsInput, options = {}) {
|
|
377
|
+
if (!Array.isArray(itemsInput) || itemsInput.length === 0) {
|
|
378
|
+
return { ok: false, reason: 'createBatch requires at least one item' };
|
|
379
|
+
}
|
|
380
|
+
const dagResult = validateDag(itemsInput);
|
|
381
|
+
if (!dagResult.ok)
|
|
382
|
+
return dagResult;
|
|
383
|
+
const clock = options.clock ?? clock_cjs_1.realClock;
|
|
384
|
+
try {
|
|
385
|
+
return withPlanningLock(cwd, () => {
|
|
386
|
+
const quickDir = quickDirFrom(planningDir(cwd));
|
|
387
|
+
const used = collectExistingQuickIds(quickDir);
|
|
388
|
+
collectExistingBatchQuickIds(cwd, used);
|
|
389
|
+
const { dateStr, startBlock } = dateAndStartBlock(new Date(clock.now()));
|
|
390
|
+
const allocated = allocateIdsGivenUsed(dateStr, startBlock, itemsInput.length + 1, used);
|
|
391
|
+
const batchId = allocated[0];
|
|
392
|
+
const quickIds = allocated.slice(1);
|
|
393
|
+
const items = itemsInput.map((input, i) => ({
|
|
394
|
+
quick_id: quickIds[i],
|
|
395
|
+
client_id: input.clientId ?? null,
|
|
396
|
+
description: input.description,
|
|
397
|
+
status: 'pending',
|
|
398
|
+
depends_on: (input.dependsOn ?? []).map((ref) => {
|
|
399
|
+
const idx = resolveDependencyIndex(ref, itemsInput);
|
|
400
|
+
return quickIds[idx];
|
|
401
|
+
}),
|
|
402
|
+
planned_files: (input.plannedFiles ?? []).map(shell_command_projection_cjs_1.posixNormalize),
|
|
403
|
+
directory: input.directory ?? null,
|
|
404
|
+
worktree: input.worktree ?? null,
|
|
405
|
+
dispatched_worktree: input.dispatchedWorktree ?? null,
|
|
406
|
+
dispatched_branch: input.dispatchedBranch ?? null,
|
|
407
|
+
dispatched_base: input.dispatchedBase ?? null,
|
|
408
|
+
wave: -1,
|
|
409
|
+
commit: null,
|
|
410
|
+
failure_reason: null,
|
|
411
|
+
}));
|
|
412
|
+
const wavesResult = computeWaves(toWaveInput(items));
|
|
413
|
+
if (!wavesResult.ok)
|
|
414
|
+
return wavesResult;
|
|
415
|
+
const waveOf = new Map();
|
|
416
|
+
wavesResult.value.forEach((wave, idx) => {
|
|
417
|
+
for (const quickId of wave)
|
|
418
|
+
waveOf.set(quickId, idx);
|
|
419
|
+
});
|
|
420
|
+
for (const it of items)
|
|
421
|
+
it.wave = waveOf.get(it.quick_id);
|
|
422
|
+
const manifest = {
|
|
423
|
+
schema_version: 1,
|
|
424
|
+
batch_id: batchId,
|
|
425
|
+
created_at: clock.nowIso(),
|
|
426
|
+
options: options.batchOptions ?? {},
|
|
427
|
+
base_revision: options.baseRevision ?? null,
|
|
428
|
+
items,
|
|
429
|
+
};
|
|
430
|
+
const manifestPath = batchManifestPath(cwd, batchId);
|
|
431
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
432
|
+
return { ok: true, value: { batchId, manifestPath, manifest } };
|
|
433
|
+
}, clock);
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// ─── BATCH.json loading + schema validation ─────────────────────────────────────
|
|
440
|
+
const VALID_STATUSES = new Set(['pending', 'complete', 'failed', 'blocked']);
|
|
441
|
+
const SAFE_BATCH_ID_RE = /^[0-9a-zA-Z-]+$/;
|
|
442
|
+
function validateBatchSchema(parsed, batchId) {
|
|
443
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
444
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} is not a JSON object` };
|
|
445
|
+
}
|
|
446
|
+
const obj = parsed;
|
|
447
|
+
if (obj.schema_version !== 1) {
|
|
448
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} has an unsupported or missing schema_version` };
|
|
449
|
+
}
|
|
450
|
+
if (typeof obj.batch_id !== 'string' || obj.batch_id !== batchId) {
|
|
451
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} has a mismatched or missing batch_id` };
|
|
452
|
+
}
|
|
453
|
+
if (typeof obj.created_at !== 'string') {
|
|
454
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} is missing created_at` };
|
|
455
|
+
}
|
|
456
|
+
if (obj.options !== undefined && (typeof obj.options !== 'object' || obj.options === null || Array.isArray(obj.options))) {
|
|
457
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} has an invalid options field` };
|
|
458
|
+
}
|
|
459
|
+
const optionsValue = obj.options ?? {};
|
|
460
|
+
if (obj.base_revision !== null && obj.base_revision !== undefined && typeof obj.base_revision !== 'string') {
|
|
461
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} has an invalid base_revision field` };
|
|
462
|
+
}
|
|
463
|
+
const baseRevisionValue = typeof obj.base_revision === 'string' ? obj.base_revision : null;
|
|
464
|
+
if (!Array.isArray(obj.items) || obj.items.length === 0) {
|
|
465
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} has no items` };
|
|
466
|
+
}
|
|
467
|
+
const items = [];
|
|
468
|
+
const seenIds = new Set();
|
|
469
|
+
for (const raw of obj.items) {
|
|
470
|
+
if (!raw || typeof raw !== 'object') {
|
|
471
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} has a malformed item` };
|
|
472
|
+
}
|
|
473
|
+
const it = raw;
|
|
474
|
+
if (typeof it.quick_id !== 'string' || !QUICK_ID_RE.test(it.quick_id)) {
|
|
475
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} has an item with an invalid quick_id` };
|
|
476
|
+
}
|
|
477
|
+
if (seenIds.has(it.quick_id)) {
|
|
478
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} has a duplicate quick_id: ${it.quick_id}` };
|
|
479
|
+
}
|
|
480
|
+
seenIds.add(it.quick_id);
|
|
481
|
+
if (typeof it.description !== 'string') {
|
|
482
|
+
return { ok: false, reason: `item ${it.quick_id} is missing description` };
|
|
483
|
+
}
|
|
484
|
+
if (typeof it.status !== 'string' || !VALID_STATUSES.has(it.status)) {
|
|
485
|
+
return { ok: false, reason: `item ${it.quick_id} has an invalid or missing status` };
|
|
486
|
+
}
|
|
487
|
+
if (!Array.isArray(it.depends_on) || !it.depends_on.every((d) => typeof d === 'string')) {
|
|
488
|
+
return { ok: false, reason: `item ${it.quick_id} has a malformed depends_on` };
|
|
489
|
+
}
|
|
490
|
+
if (!Array.isArray(it.planned_files) || !it.planned_files.every((f) => typeof f === 'string')) {
|
|
491
|
+
return { ok: false, reason: `item ${it.quick_id} has a malformed planned_files` };
|
|
492
|
+
}
|
|
493
|
+
if (it.directory !== null && it.directory !== undefined && typeof it.directory !== 'string') {
|
|
494
|
+
return { ok: false, reason: `item ${it.quick_id} has an invalid directory field` };
|
|
495
|
+
}
|
|
496
|
+
if (it.worktree !== null && it.worktree !== undefined && typeof it.worktree !== 'string') {
|
|
497
|
+
return { ok: false, reason: `item ${it.quick_id} has an invalid worktree field` };
|
|
498
|
+
}
|
|
499
|
+
if (typeof it.worktree === 'string' && it.worktree !== '' && !node_fs_1.default.existsSync(it.worktree)) {
|
|
500
|
+
return { ok: false, reason: `item ${it.quick_id} references a worktree that does not exist on disk: ${it.worktree}` };
|
|
501
|
+
}
|
|
502
|
+
// #3677: dispatched_worktree/branch/base deliberately carry NO
|
|
503
|
+
// existence check (unlike `worktree` above) — they must stay readable
|
|
504
|
+
// after a legitimate post-merge worktree removal.
|
|
505
|
+
if (it.dispatched_worktree !== null && it.dispatched_worktree !== undefined && typeof it.dispatched_worktree !== 'string') {
|
|
506
|
+
return { ok: false, reason: `item ${it.quick_id} has an invalid dispatched_worktree field` };
|
|
507
|
+
}
|
|
508
|
+
if (it.dispatched_branch !== null && it.dispatched_branch !== undefined && typeof it.dispatched_branch !== 'string') {
|
|
509
|
+
return { ok: false, reason: `item ${it.quick_id} has an invalid dispatched_branch field` };
|
|
510
|
+
}
|
|
511
|
+
if (it.dispatched_base !== null && it.dispatched_base !== undefined && typeof it.dispatched_base !== 'string') {
|
|
512
|
+
return { ok: false, reason: `item ${it.quick_id} has an invalid dispatched_base field` };
|
|
513
|
+
}
|
|
514
|
+
if (it.wave !== undefined && (typeof it.wave !== 'number' || !Number.isInteger(it.wave) || it.wave < 0)) {
|
|
515
|
+
return { ok: false, reason: `item ${it.quick_id} has an invalid wave field` };
|
|
516
|
+
}
|
|
517
|
+
if (it.commit !== null && it.commit !== undefined && typeof it.commit !== 'string') {
|
|
518
|
+
return { ok: false, reason: `item ${it.quick_id} has an invalid commit field` };
|
|
519
|
+
}
|
|
520
|
+
if (it.failure_reason !== null && it.failure_reason !== undefined && typeof it.failure_reason !== 'string') {
|
|
521
|
+
return { ok: false, reason: `item ${it.quick_id} has an invalid failure_reason field` };
|
|
522
|
+
}
|
|
523
|
+
items.push({
|
|
524
|
+
quick_id: it.quick_id,
|
|
525
|
+
client_id: typeof it.client_id === 'string' ? it.client_id : null,
|
|
526
|
+
description: it.description,
|
|
527
|
+
status: it.status,
|
|
528
|
+
depends_on: it.depends_on,
|
|
529
|
+
planned_files: it.planned_files,
|
|
530
|
+
directory: typeof it.directory === 'string' ? it.directory : null,
|
|
531
|
+
worktree: typeof it.worktree === 'string' ? it.worktree : null,
|
|
532
|
+
dispatched_worktree: typeof it.dispatched_worktree === 'string' ? it.dispatched_worktree : null,
|
|
533
|
+
dispatched_branch: typeof it.dispatched_branch === 'string' ? it.dispatched_branch : null,
|
|
534
|
+
dispatched_base: typeof it.dispatched_base === 'string' ? it.dispatched_base : null,
|
|
535
|
+
wave: typeof it.wave === 'number' ? it.wave : -1,
|
|
536
|
+
commit: typeof it.commit === 'string' ? it.commit : null,
|
|
537
|
+
failure_reason: typeof it.failure_reason === 'string' ? it.failure_reason : null,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
for (const it of items) {
|
|
541
|
+
for (const dep of it.depends_on) {
|
|
542
|
+
if (!seenIds.has(dep)) {
|
|
543
|
+
return { ok: false, reason: `item ${it.quick_id} depends on ${dep}, which is not present in this batch` };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const cycleCheck = computeWaves(toWaveInput(items));
|
|
548
|
+
if (!cycleCheck.ok)
|
|
549
|
+
return cycleCheck;
|
|
550
|
+
return {
|
|
551
|
+
ok: true,
|
|
552
|
+
value: {
|
|
553
|
+
schema_version: 1,
|
|
554
|
+
batch_id: batchId,
|
|
555
|
+
created_at: obj.created_at,
|
|
556
|
+
options: optionsValue,
|
|
557
|
+
base_revision: baseRevisionValue,
|
|
558
|
+
items,
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Load and schema-validate `BATCH.json` for `batchId`. Fails closed —
|
|
564
|
+
* never guesses partial state — on: missing file, corrupt/truncated JSON,
|
|
565
|
+
* a schema violation, an out-of-batch dependency reference, a dependency
|
|
566
|
+
* cycle, or an item referencing a worktree path absent from disk.
|
|
567
|
+
*/
|
|
568
|
+
function loadBatch(cwd, batchId) {
|
|
569
|
+
if (typeof batchId !== 'string' || batchId === '' || !SAFE_BATCH_ID_RE.test(batchId)) {
|
|
570
|
+
return { ok: false, reason: `invalid batch id: ${String(batchId)}` };
|
|
571
|
+
}
|
|
572
|
+
const manifestPath = batchManifestPath(cwd, batchId);
|
|
573
|
+
let raw;
|
|
574
|
+
try {
|
|
575
|
+
raw = node_fs_1.default.readFileSync(manifestPath, 'utf-8');
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
const e = err;
|
|
579
|
+
return {
|
|
580
|
+
ok: false,
|
|
581
|
+
reason: e.code === 'ENOENT'
|
|
582
|
+
? `no BATCH.json found for batch ${batchId}`
|
|
583
|
+
: `unable to read BATCH.json for batch ${batchId}: ${e.message}`,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
const parsed = (0, security_cjs_1.safeJsonParse)(raw, { maxLength: 1048576 });
|
|
587
|
+
if (!parsed.ok) {
|
|
588
|
+
return { ok: false, reason: `BATCH.json for batch ${batchId} is not valid JSON: ${parsed.error ?? 'unknown parse error'}` };
|
|
589
|
+
}
|
|
590
|
+
return validateBatchSchema(parsed.value, batchId);
|
|
591
|
+
}
|
|
592
|
+
// ─── Exactly-once STATE.md completion ───────────────────────────────────────────
|
|
593
|
+
const QUICK_TASKS_HEADING_RE = /^quick tasks completed\b/i;
|
|
594
|
+
/**
|
|
595
|
+
* Idempotency check backing exactly-once STATE completion: does a "Quick
|
|
596
|
+
* Tasks Completed" table row already carry this `quickId` in its `#` column?
|
|
597
|
+
* Mirrors `appendQuickTaskRow`'s own section/table resolution (heading match
|
|
598
|
+
* -> parseMarkdownTable -> matchTableSchema) without modifying it, so the two
|
|
599
|
+
* can never silently diverge on what counts as "the" Quick Tasks table.
|
|
600
|
+
*/
|
|
601
|
+
function hasQuickTaskRow(stateContent, quickId) {
|
|
602
|
+
if (!stateContent)
|
|
603
|
+
return false;
|
|
604
|
+
const sections = (0, markdown_sectionizer_cjs_1.collectSections)(stateContent, (h) => QUICK_TASKS_HEADING_RE.test(h.text.trim()));
|
|
605
|
+
for (const section of sections) {
|
|
606
|
+
const parsed = (0, markdown_table_cjs_1.parseMarkdownTable)(section.body);
|
|
607
|
+
if (!parsed.ok)
|
|
608
|
+
continue;
|
|
609
|
+
const match = (0, markdown_table_cjs_1.matchTableSchema)(parsed.value.columns);
|
|
610
|
+
if (!match || match.id !== 'QuickTasks')
|
|
611
|
+
continue;
|
|
612
|
+
for (const row of parsed.value.rows) {
|
|
613
|
+
if (row['#'] === quickId)
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Mark one batch item complete: appends exactly one STATE.md "Quick Tasks
|
|
621
|
+
* Completed" row (idempotency key = `quickId`, checked via `hasQuickTaskRow`
|
|
622
|
+
* BEFORE calling `appendQuickTaskRow` — that function itself has no
|
|
623
|
+
* idempotency of its own), then marks the item `complete` in `BATCH.json`.
|
|
624
|
+
* Both steps run inside ONE `withPlanningLock` transaction. A repeat call for
|
|
625
|
+
* an already-complete item is a no-op (no re-append, no re-write).
|
|
626
|
+
*/
|
|
627
|
+
function completeQuickItem(cwd, batchId, quickId, fields, options = {}) {
|
|
628
|
+
const clock = options.clock ?? clock_cjs_1.realClock;
|
|
629
|
+
try {
|
|
630
|
+
return withPlanningLock(cwd, () => {
|
|
631
|
+
const loaded = loadBatch(cwd, batchId);
|
|
632
|
+
if (!loaded.ok)
|
|
633
|
+
return loaded;
|
|
634
|
+
const manifest = loaded.value;
|
|
635
|
+
const item = manifest.items.find((it) => it.quick_id === quickId);
|
|
636
|
+
if (!item)
|
|
637
|
+
return { ok: false, reason: `batch ${batchId} has no item ${quickId}` };
|
|
638
|
+
const statePath = planningPaths(cwd).state;
|
|
639
|
+
const stateContent = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : '';
|
|
640
|
+
let appended = false;
|
|
641
|
+
if (!hasQuickTaskRow(stateContent, quickId)) {
|
|
642
|
+
const appendResult = (0, markdown_table_cjs_1.appendQuickTaskRow)(stateContent, {
|
|
643
|
+
description: fields.description,
|
|
644
|
+
date: fields.date,
|
|
645
|
+
commit: fields.commit,
|
|
646
|
+
directory: fields.directory,
|
|
647
|
+
quickId,
|
|
648
|
+
});
|
|
649
|
+
if (!appendResult.ok)
|
|
650
|
+
return appendResult;
|
|
651
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(statePath, appendResult.value.content);
|
|
652
|
+
appended = true;
|
|
653
|
+
}
|
|
654
|
+
if (item.status !== 'complete') {
|
|
655
|
+
item.status = 'complete';
|
|
656
|
+
item.commit = fields.commit;
|
|
657
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(batchManifestPath(cwd, batchId), JSON.stringify(manifest, null, 2) + '\n');
|
|
658
|
+
}
|
|
659
|
+
return { ok: true, value: { appended, manifest } };
|
|
660
|
+
}, clock);
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Persist post-planning `depends_on`/`planned_files` updates and recompute
|
|
668
|
+
* `wave` for every item (Phase 4 / #3676, resolving design doc Open
|
|
669
|
+
* Question 1's "no mutator exists" gap as ONE additive export on this
|
|
670
|
+
* module — never a second, independent writer against the same
|
|
671
|
+
* `BATCH.json`). Runs inside ONE `withPlanningLock` transaction, reusing
|
|
672
|
+
* the exact `loadBatch` -> mutate -> `computeWaves` -> `platformWriteSync`
|
|
673
|
+
* shape `resumeBatch`/`completeQuickItem` already use.
|
|
674
|
+
*
|
|
675
|
+
* Fails closed WITHOUT persisting anything when an update references an
|
|
676
|
+
* unknown `quickId`, an unknown/self dependency, or the resulting graph has
|
|
677
|
+
* a cycle — `computeWaves` (reused, not duplicated) is the single source of
|
|
678
|
+
* truth for that validation, exactly as it is at `createBatch` time.
|
|
679
|
+
*/
|
|
680
|
+
function updateBatchItems(cwd, batchId, updates, options = {}) {
|
|
681
|
+
const clock = options.clock ?? clock_cjs_1.realClock;
|
|
682
|
+
try {
|
|
683
|
+
return withPlanningLock(cwd, () => {
|
|
684
|
+
const loaded = loadBatch(cwd, batchId);
|
|
685
|
+
if (!loaded.ok)
|
|
686
|
+
return loaded;
|
|
687
|
+
const manifest = loaded.value;
|
|
688
|
+
const byId = new Map(manifest.items.map((it) => [it.quick_id, it]));
|
|
689
|
+
for (const update of updates) {
|
|
690
|
+
const item = byId.get(update.quickId);
|
|
691
|
+
if (!item) {
|
|
692
|
+
return { ok: false, reason: `batch ${batchId} has no item ${update.quickId}` };
|
|
693
|
+
}
|
|
694
|
+
if (update.dependsOn !== undefined) {
|
|
695
|
+
for (const dep of update.dependsOn) {
|
|
696
|
+
if (dep === update.quickId) {
|
|
697
|
+
return { ok: false, reason: `item ${update.quickId} declares a dependency on itself` };
|
|
698
|
+
}
|
|
699
|
+
if (!byId.has(dep)) {
|
|
700
|
+
return { ok: false, reason: `item ${update.quickId} declares an unknown dependency reference: ${JSON.stringify(dep)}` };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
item.depends_on = [...update.dependsOn];
|
|
704
|
+
}
|
|
705
|
+
if (update.plannedFiles !== undefined) {
|
|
706
|
+
item.planned_files = update.plannedFiles.map(shell_command_projection_cjs_1.posixNormalize);
|
|
707
|
+
}
|
|
708
|
+
// #3677: durable worktree-recovery fields — persisted verbatim, no
|
|
709
|
+
// existence/shape validation (opaque identifiers this module never
|
|
710
|
+
// interprets, same as `commit`). Explicit `null` clears a field
|
|
711
|
+
// (post-merge); `undefined` leaves it untouched, same convention
|
|
712
|
+
// `dependsOn`/`plannedFiles` above already use.
|
|
713
|
+
if (update.dispatchedWorktree !== undefined) {
|
|
714
|
+
item.dispatched_worktree = update.dispatchedWorktree;
|
|
715
|
+
}
|
|
716
|
+
if (update.dispatchedBranch !== undefined) {
|
|
717
|
+
item.dispatched_branch = update.dispatchedBranch;
|
|
718
|
+
}
|
|
719
|
+
if (update.dispatchedBase !== undefined) {
|
|
720
|
+
item.dispatched_base = update.dispatchedBase;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const wavesResult = computeWaves(toWaveInput(manifest.items));
|
|
724
|
+
if (!wavesResult.ok)
|
|
725
|
+
return wavesResult;
|
|
726
|
+
const waveOf = new Map();
|
|
727
|
+
wavesResult.value.forEach((wave, idx) => {
|
|
728
|
+
for (const quickId of wave)
|
|
729
|
+
waveOf.set(quickId, idx);
|
|
730
|
+
});
|
|
731
|
+
for (const it of manifest.items)
|
|
732
|
+
it.wave = waveOf.get(it.quick_id);
|
|
733
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(batchManifestPath(cwd, batchId), JSON.stringify(manifest, null, 2) + '\n');
|
|
734
|
+
return { ok: true, value: { manifest } };
|
|
735
|
+
}, clock);
|
|
736
|
+
}
|
|
737
|
+
catch (err) {
|
|
738
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Resume a batch: skips `complete` items, leaves `failed` items failed (no
|
|
743
|
+
* auto-retry), leaves `blocked` items blocked unless their dependency's
|
|
744
|
+
* outcome changed, and — crash-window safety — detects a STATE.md row that
|
|
745
|
+
* already exists (by `quickId`, via `hasQuickTaskRow`) for a non-complete
|
|
746
|
+
* item and marks it complete WITHOUT re-appending (the "STATE row written,
|
|
747
|
+
* BATCH.json not yet updated" crash window). Idempotent: two calls in a row
|
|
748
|
+
* on an unchanged manifest produce zero transitions the second time.
|
|
749
|
+
*
|
|
750
|
+
* Runs inside `withPlanningLock` — the same durable read-modify-write
|
|
751
|
+
* `BATCH.json` uses — so a resume racing a concurrent `completeQuickItem` (or
|
|
752
|
+
* another resume) can never lose an update.
|
|
753
|
+
*
|
|
754
|
+
* When `options.currentBaseRevision` is supplied and the manifest carries a
|
|
755
|
+
* non-null `base_revision` that differs from it, resume refuses with a
|
|
756
|
+
* recoverable diagnostic rather than guessing past the divergence (ADR-1239
|
|
757
|
+
* "Quick-batch binding" § Base divergence). Reconciling the divergence — a
|
|
758
|
+
* rebase, a fresh batch — is a caller (Phase 4) decision; this primitive only
|
|
759
|
+
* detects and reports it. Omit the option to skip the check entirely.
|
|
760
|
+
*/
|
|
761
|
+
function resumeBatch(cwd, batchId, options = {}) {
|
|
762
|
+
const clock = options.clock ?? clock_cjs_1.realClock;
|
|
763
|
+
try {
|
|
764
|
+
return withPlanningLock(cwd, () => {
|
|
765
|
+
const loaded = loadBatch(cwd, batchId);
|
|
766
|
+
if (!loaded.ok)
|
|
767
|
+
return loaded;
|
|
768
|
+
const manifest = loaded.value;
|
|
769
|
+
if (options.currentBaseRevision !== undefined
|
|
770
|
+
&& manifest.base_revision !== null
|
|
771
|
+
&& manifest.base_revision !== options.currentBaseRevision) {
|
|
772
|
+
return {
|
|
773
|
+
ok: false,
|
|
774
|
+
reason: `batch ${batchId} base revision diverged: created against ${manifest.base_revision}, current is ${options.currentBaseRevision}`,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const statePath = planningPaths(cwd).state;
|
|
778
|
+
const stateContent = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : '';
|
|
779
|
+
const transitions = [];
|
|
780
|
+
const byId = new Map(manifest.items.map((it) => [it.quick_id, it]));
|
|
781
|
+
// Crash-window detection: a non-complete item whose STATE row already
|
|
782
|
+
// exists is completed without re-appending.
|
|
783
|
+
for (const it of manifest.items) {
|
|
784
|
+
if (it.status !== 'complete' && hasQuickTaskRow(stateContent, it.quick_id)) {
|
|
785
|
+
transitions.push({ quickId: it.quick_id, from: it.status, to: 'complete' });
|
|
786
|
+
it.status = 'complete';
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// Propagate blocked/failed along the DAG to a fixed point (bounded by
|
|
790
|
+
// item count) so transitive blocking resolves in one resume call. A
|
|
791
|
+
// `blocked` item whose dependency outcome improved reverts to `pending`
|
|
792
|
+
// (eligible for re-evaluation); a `failed` item is never touched (no
|
|
793
|
+
// auto-retry).
|
|
794
|
+
let changed = true;
|
|
795
|
+
let iterations = 0;
|
|
796
|
+
while (changed && iterations <= manifest.items.length) {
|
|
797
|
+
changed = false;
|
|
798
|
+
iterations++;
|
|
799
|
+
for (const it of manifest.items) {
|
|
800
|
+
if (it.status === 'complete' || it.status === 'failed')
|
|
801
|
+
continue;
|
|
802
|
+
const anyDepBad = it.depends_on.some((d) => {
|
|
803
|
+
const dep = byId.get(d);
|
|
804
|
+
return dep !== undefined && (dep.status === 'failed' || dep.status === 'blocked');
|
|
805
|
+
});
|
|
806
|
+
const nextStatus = anyDepBad ? 'blocked' : (it.status === 'blocked' ? 'pending' : it.status);
|
|
807
|
+
if (nextStatus !== it.status) {
|
|
808
|
+
transitions.push({ quickId: it.quick_id, from: it.status, to: nextStatus });
|
|
809
|
+
it.status = nextStatus;
|
|
810
|
+
changed = true;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const eligible = manifest.items
|
|
815
|
+
.filter((it) => it.status === 'pending' && it.depends_on.every((d) => byId.get(d)?.status === 'complete'))
|
|
816
|
+
.map((it) => it.quick_id);
|
|
817
|
+
if (transitions.length > 0) {
|
|
818
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(batchManifestPath(cwd, batchId), JSON.stringify(manifest, null, 2) + '\n');
|
|
819
|
+
}
|
|
820
|
+
return { ok: true, value: { eligible, transitions, manifest } };
|
|
821
|
+
}, clock);
|
|
822
|
+
}
|
|
823
|
+
catch (err) {
|
|
824
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
module.exports = {
|
|
828
|
+
parseTaskList,
|
|
829
|
+
parseTaskListFromFile,
|
|
830
|
+
allocateQuickIds,
|
|
831
|
+
allocateIdsGivenUsed,
|
|
832
|
+
MAX_TIME_BLOCK,
|
|
833
|
+
createBatch,
|
|
834
|
+
loadBatch,
|
|
835
|
+
computeWaves,
|
|
836
|
+
resumeBatch,
|
|
837
|
+
completeQuickItem,
|
|
838
|
+
hasQuickTaskRow,
|
|
839
|
+
updateBatchItems,
|
|
840
|
+
};
|