@tekyzinc/gsd-t 3.16.12 → 3.18.11

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +13 -3
  3. package/bin/gsd-t-depgraph-validate.cjs +140 -0
  4. package/bin/gsd-t-economics.cjs +287 -0
  5. package/bin/gsd-t-file-disjointness.cjs +227 -0
  6. package/bin/gsd-t-in-session-usage.cjs +213 -0
  7. package/bin/gsd-t-orchestrator-config.cjs +100 -3
  8. package/bin/gsd-t-orchestrator.js +2 -1
  9. package/bin/gsd-t-parallel.cjs +382 -0
  10. package/bin/gsd-t-report-tokens.cjs +549 -0
  11. package/bin/gsd-t-task-graph.cjs +366 -0
  12. package/bin/gsd-t-token-capture.cjs +29 -14
  13. package/bin/gsd-t-token-dashboard.cjs +35 -0
  14. package/bin/gsd-t-tool-attribution.cjs +377 -0
  15. package/bin/gsd-t-tool-cost.cjs +195 -0
  16. package/bin/gsd-t-unattended-platform.cjs +7 -1
  17. package/bin/gsd-t-unattended.cjs +2 -0
  18. package/bin/gsd-t.js +155 -5
  19. package/bin/headless-auto-spawn.cjs +69 -49
  20. package/bin/headless-auto-spawn.js +18 -24
  21. package/bin/runway-estimator.cjs +212 -0
  22. package/bin/spawn-plan-derive.cjs +163 -0
  23. package/bin/spawn-plan-status-updater.cjs +292 -0
  24. package/bin/spawn-plan-writer.cjs +204 -0
  25. package/commands/gsd-t-debug.md +26 -7
  26. package/commands/gsd-t-execute.md +36 -28
  27. package/commands/gsd-t-help.md +11 -0
  28. package/commands/gsd-t-integrate.md +27 -7
  29. package/commands/gsd-t-quick.md +30 -13
  30. package/commands/gsd-t-scan.md +5 -5
  31. package/commands/gsd-t-unattended-watch.md +4 -3
  32. package/commands/gsd-t-unattended.md +9 -3
  33. package/commands/gsd-t-verify.md +5 -5
  34. package/commands/gsd-t-wave.md +21 -8
  35. package/commands/gsd.md +45 -3
  36. package/docs/GSD-T-README.md +43 -5
  37. package/docs/architecture.md +423 -3
  38. package/docs/requirements.md +203 -0
  39. package/package.json +1 -1
  40. package/scripts/gsd-t-calibration-hook.js +256 -0
  41. package/scripts/gsd-t-compact-detector.js +223 -0
  42. package/scripts/gsd-t-compaction-scanner.js +305 -0
  43. package/scripts/gsd-t-dashboard-autostart.cjs +172 -0
  44. package/scripts/gsd-t-dashboard-server.js +179 -0
  45. package/scripts/gsd-t-heartbeat.js +50 -2
  46. package/scripts/gsd-t-post-commit-spawn-plan.sh +86 -0
  47. package/scripts/gsd-t-transcript.html +546 -43
  48. package/scripts/hooks/gsd-t-in-session-usage-hook.js +84 -0
  49. package/scripts/spawn-plan-fmt-tokens.cjs +80 -0
  50. package/templates/CLAUDE-global.md +8 -3
  51. package/templates/CLAUDE-project.md +17 -14
  52. package/templates/hooks/post-commit-spawn-plan.sh +85 -0
@@ -461,3 +461,206 @@ Supporting contracts (no direct REQ mapping — shared infrastructure):
461
461
  - `stream-json-sink-contract.md` (d1↔d4 joint) — enables REQ-M40-03
462
462
 
463
463
  All 5 REQs map to at least one task; no orphaned requirements. All 25 tasks across 7 domains trace to at least one REQ (task-brief/completion tasks support via contract infra).
464
+
465
+ ## M43 Universal Token Attribution (partition phase — 2026-04-21)
466
+
467
+ Milestone 43 (Token Attribution & Always-Headless Inversion — target v3.17.10) decomposes into 6 measurable requirements across two themes. Task numbers reference `.gsd-t/domains/m43-*/tasks.md`.
468
+
469
+ | REQ-ID | Requirement Summary | Domain | Status |
470
+ |--------|---------------------|--------|--------|
471
+ | REQ-M43-01 | Per-turn in-session token usage captured on every dialog turn; rows land in `.gsd-t/metrics/token-usage.jsonl` with `turn_id`, `session_id`, `sessionType: "in-session"` | D1 in-session-usage-capture | **Wave 1 complete (2026-04-21)** — Branch B locked (transcript-sourced, Stop-hook-triggered). Live end-to-end validated: 523 rows in sink from real transcript. |
472
+ | REQ-M43-02 | Per-tool attribution via output-byte ratio — `gsd-t tool-cost --group-by tool\|command\|domain` shows non-zero attribution across Bash/Read/Edit/Grep/Task | D2 per-tool-attribution | pending (Wave 2) |
473
+ | REQ-M43-03 | One canonical sink + schema v2 for all token rows; `.gsd-t/token-log.md` regeneratable view | D3 sink-unification-backfill | **complete (2026-04-21)** minus D3-T4.1 backfill parser follow-up |
474
+ | REQ-M43-04 | Every command spawns; zero `--in-session` / `--headless` flag matches in `commands/*.md` after D4 | D4 default-headless-inversion | pending (Wave 3) |
475
+ | REQ-M43-05 | Router dialog-growth meter warns when `/compact` predicted within N turns; pure read/warn, never refuses | D5 dialog-channel-meter | pending (Wave 2) |
476
+ | REQ-M43-06 | Dashboard URL printed at every spawn; transcript viewer auto-launches on first spawn if not running; tool-cost panel renders against any spawn | D6 transcript-viewer-primary-surface | pending (Wave 2) |
477
+
478
+ ### D1 Branch Lock (2026-04-21)
479
+
480
+ Branch B chosen. Evidence from `.gsd-t/.hook-probe/` payloads (captured during supervisor-2026-04-21-2320 session):
481
+
482
+ - Stop / SessionEnd / PostToolUse hook payloads carry `session_id`, `transcript_path`, `cwd`, `hook_event_name` — **but no `usage`**.
483
+ - `transcript_path` JSONL rows contain `message.usage` with `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, `total_cost_usd` — same shape the M40 D4 aggregator already parses.
484
+
485
+ **Hybrid chosen**: Stop hook = trigger, transcript = data source. `bin/gsd-t-in-session-usage.cjs::processHookPayload` reads the transcript since the last cursor, emits one v2-schema JSONL row per assistant turn with `sessionType: "in-session"`, `turn_id` (Claude `message.id`), `session_id` (hook `session_id`). Idempotent across replays via per-session transcript-line cursor.
486
+
487
+ **Install path**: hook entry goes in user's `~/.claude/settings.json` (or project `.claude/settings.json`) — not written by the installer. The user wires it once:
488
+
489
+ ```json
490
+ { "matcher": "", "hooks": [
491
+ { "type": "command",
492
+ "command": "node \"$HOME/.claude/scripts/gsd-t-in-session-usage-hook.js\"",
493
+ "async": true } ] }
494
+ ```
495
+
496
+ Supporting contracts:
497
+ - `metrics-schema-contract.md` v2 — owns `turn_id`, `session_id`, `sessionType`, `tool_attribution[]`
498
+ - `stream-json-sink-contract.md` v1.2.0 — formalizes dialog-channel entry-point (D1)
499
+ - `tool-attribution-contract.md` v1.0.0 — output-byte ratio algorithm (D2, new contract)
500
+ - `headless-default-contract.md` v2.0.0 — always-headless rule (D4, bump pending)
501
+
502
+ ## M44 Task-Graph Reader (D1 — execute phase 2026-04-22)
503
+
504
+ Milestone 44 D1 ships the shared DAG that all downstream M44 domains (D2 parallel CLI, D3 command-file integration, D4 dep-graph validation, D5 file-disjointness prover, D6 pre-spawn economics) consume. Mode-agnostic: produces only a graph, owns no in-session vs unattended branching.
505
+
506
+ | REQ-ID | Requirement Summary | Domain | Status |
507
+ |--------|---------------------|--------|--------|
508
+ | REQ-M44-D1-01 | `bin/gsd-t-task-graph.cjs` parses every `.gsd-t/domains/*/tasks.md` into a typed in-memory DAG: nodes `{id, domain, wave, title, status, deps, touches}`, edges `{from, to}`, `ready` mask, `byId` index, `warnings[]` | m44-d1-task-graph-reader | **complete (2026-04-22)** — 22/22 unit tests pass |
509
+ | REQ-M44-D1-02 | Cycle detection mandatory: throws `TaskGraphCycleError` with `.cycle: string[]` path on any circular dep — 3-task ring AND self-loop covered | m44-d1-task-graph-reader | **complete (2026-04-22)** — iterative three-color DFS; tests 3 + 4 |
510
+ | REQ-M44-D1-03 | Touch-list resolution: prefer task `**Touches**` / `**Files touched**`, fall back to domain `scope.md` `## Files Owned`, fall back to `[]` + warning | m44-d1-task-graph-reader | **complete (2026-04-22)** — both fallback paths tested |
511
+ | REQ-M44-D1-04 | Status markers: `[ ]` pending / `[x]` done / `[-]` skipped / `[!]` failed; unknown markers → pending + warning. Only `done` deps satisfy dependents (skipped/failed do NOT) | m44-d1-task-graph-reader | **complete (2026-04-22)** — covered by ready-mask tests |
512
+ | REQ-M44-D1-05 | Read-only invariant: never writes to `tasks.md`, `scope.md`, or any contract file during build/query | m44-d1-task-graph-reader | **complete (2026-04-22)** — sync `readFileSync` only, no `writeFileSync` calls |
513
+ | REQ-M44-D1-06 | Performance: parse + cycle-check + ready-mask in < 200 ms for 100-domain / 1000-task project | m44-d1-task-graph-reader | **complete (2026-04-22)** — measured 6 ms on 50-domain/250-task synthetic; 3 ms on this repo |
514
+ | REQ-M44-D1-07 | CLI debugging surface: `gsd-t graph --output json` (pretty JSON) and `--output table` (id/domain/wave/status/ready/deps), backward-compat with existing `graph index/status/query` | m44-d1-task-graph-reader | **complete (2026-04-22)** — verified live: 33 tasks · 36 edges · 2 ready |
515
+
516
+ Supporting contract:
517
+ - `task-graph-contract.md` v1.0.0 — locks DAG schema; downstream M44 domains may begin implementation against this contract.
518
+
519
+ ## M44 Dep-Graph Validation (D4 — Wave 2 gate, 2026-04-22)
520
+
521
+ Milestone 44 D4 ships the pre-spawn dependency gate. Given the DAG from D1 (`buildTaskGraph`), it filters the candidate-ready set to tasks whose declared `deps[]` are **all** in DONE status, and emits a `dep_gate_veto` event for every task it removes. The consumer (D2) decides whether to spawn a smaller batch or fall back to sequential — D4 itself is a pure filter. Mode-agnostic: same call shape in-session and unattended.
522
+
523
+ | REQ-ID | Requirement Summary | Domain | Status |
524
+ |--------|---------------------|--------|--------|
525
+ | REQ-M44-D4-01 | `bin/gsd-t-depgraph-validate.cjs` exports `validateDepGraph({graph, projectDir}) → {ready: TaskNode[], vetoed: {task, unmet_deps[]}[]}`. Synchronous. Never throws on unmet deps. | m44-d4-depgraph-validation | **complete (2026-04-22)** — 13/13 unit tests pass |
526
+ | REQ-M44-D4-02 | Veto rule: a dep is satisfied iff `graph.byId[depId]` exists AND `status === 'done'`. Pending / skipped / failed / unknown all veto. (Matches `task-graph-contract.md` §5.) | m44-d4-depgraph-validation | **complete (2026-04-22)** — covered by skipped-dep, failed-dep, and unknown-dep tests |
527
+ | REQ-M44-D4-03 | For every vetoed task, append one `dep_gate_veto` event to `.gsd-t/events/YYYY-MM-DD.jsonl` carrying the full base event schema (ts ISO 8601, event_type, command/phase/agent_id/parent_agent_id/trace_id/model null, reasoning="unmet deps: …", outcome="deferred") PLUS D4-additive fields `task_id`, `domain`, `unmet_deps[]` | m44-d4-depgraph-validation | **complete (2026-04-22)** — event-schema assertion test covers all fields |
528
+ | REQ-M44-D4-04 | Non-throwing guarantee: unmet deps, unknown dep ids, and event-log I/O failures NEVER throw. Only malformed `opts` (missing `graph`) throws — programming error only. | m44-d4-depgraph-validation | **complete (2026-04-22)** — 20-task stress test + 3 throw-guard tests |
529
+ | REQ-M44-D4-05 | Read-only on all domain artifacts. Only write surface is appending JSONL lines to `.gsd-t/events/YYYY-MM-DD.jsonl` (events directory created on demand). Zero external runtime deps. | m44-d4-depgraph-validation | **complete (2026-04-22)** — only `appendFileSync`/`mkdirSync` on events dir |
530
+ | REQ-M44-D4-06 | Performance: adds < 50 ms to the pre-spawn path on a realistic 100-domain / 1000-task graph. O(R · D) where R = |candidate set|, D = avg deps/task. | m44-d4-depgraph-validation | **complete (2026-04-22)** — test suite runs in < 50 ms total |
531
+ | REQ-M44-D4-07 | Synthetic gate fixture: a task with one unmet dep (unknown id) is vetoed; an independent task in the same ready set passes through. | m44-d4-depgraph-validation | **complete (2026-04-22)** — `M44-D4 gate fixture` test |
532
+
533
+ Supporting contract:
534
+ - `depgraph-validation-contract.md` v1.0.0 — locks veto semantics, dep_gate_veto event payload, non-throw guarantee, read-only invariant, and the D4 → D5 → D6 pre-spawn pipeline ordering. Downstream D2/D3 may wire in.
535
+
536
+ ## M44 File-Disjointness Prover (D5 — Wave 2 gate, 2026-04-22)
537
+
538
+ Milestone 44 D5 ships the pre-spawn gate that proves every candidate parallel-spawn set writes disjoint files. If two tasks would both write the same file, D5 removes them from the parallel set and routes them to a sequential queue. Unprovable tasks (no touch-list source) are always routed sequential as singletons — safe-default, never assume disjoint. Mode-agnostic: same function used by in-session (D2) and unattended (D6) consumers.
539
+
540
+ | REQ-ID | Requirement Summary | Domain | Status |
541
+ |--------|---------------------|--------|--------|
542
+ | REQ-M44-D5-01 | `bin/gsd-t-file-disjointness.cjs` exports `proveDisjointness({tasks, projectDir})` → `{parallel: TaskNode[][], sequential: TaskNode[][], unprovable: TaskNode[]}`. Synchronous. Never throws. | m44-d5-file-disjointness-prover | **complete (2026-04-22)** — 11/11 unit tests pass |
543
+ | REQ-M44-D5-02 | Touch-list source priority: (1) explicit `touches` populated by D1, (2) git-history heuristic bounded to 100 commits via `git log --name-only -n 100 -- .gsd-t/domains/<domain>/` intersecting with commit subjects containing the task id, (3) unprovable → always sequential | m44-d5-file-disjointness-prover | **complete (2026-04-22)** — fallback chain covered by `_resolveTouches` tests |
544
+ | REQ-M44-D5-03 | Union-find grouping over the pairwise write-target overlap relation: component size 1 → parallel; component size ≥ 2 → sequential (transitive closure) | m44-d5-file-disjointness-prover | **complete (2026-04-22)** — 3-task transitive-closure test verified |
545
+ | REQ-M44-D5-04 | For every task routed sequential (including unprovable singletons), append `{type:'disjointness_fallback', task_id, reason, ts}` to `.gsd-t/events/YYYY-MM-DD.jsonl`. Reasons: `unprovable` or `write-target-overlap`. Best-effort (silent on FS failure). | m44-d5-file-disjointness-prover | **complete (2026-04-22)** — event shape asserted in 3 tests |
546
+ | REQ-M44-D5-05 | Read-only on all domain artifacts. Only write surface is the event stream. Git subprocess wrapped in try/catch. Zero external runtime deps. | m44-d5-file-disjointness-prover | **complete (2026-04-22)** |
547
+ | REQ-M44-D5-06 | Robust input handling: `opts.tasks` missing/null, task missing `touches` field, empty candidate set — all return a valid partition without throwing | m44-d5-file-disjointness-prover | **complete (2026-04-22)** — robustness tests cover all four paths |
548
+
549
+ Supporting contract:
550
+ - `file-disjointness-contract.md` v1.0.0 — locks prover interface, fallback chain, and event format. Downstream D2/D6 may wire in.
551
+
552
+ ## M44 Per-CW Attribution (D7 — Wave 1 foundation, 2026-04-22)
553
+
554
+ Milestone 44 D7 ships the per-Context-Window attribution surface that downstream consumers (D6 estimator calibration, the per-CW rollup in `gsd-t metrics`, the optimization report) need to keep working post-M44. Without `cw_id` on token-usage rows, every iter looks like a single CW even when Claude Code compacted mid-run; the calibration loop also needs a post-spawn signal so D6 can self-correct from the delta between predicted and observed CW utilization.
555
+
556
+ | REQ-ID | Requirement Summary | Domain | Status |
557
+ |--------|---------------------|--------|--------|
558
+ | REQ-M44-D7-01 | `bin/gsd-t-token-capture.cjs` accepts an optional `cw_id` field on `recordSpawnRow` / `captureSpawn`. When supplied, written to the JSONL row; when absent, omitted (NOT null, NOT ""). Pass-through only — wrapper does not derive `cw_id`. | m44-d7-per-cw-attribution | **complete (2026-04-22)** — 15/15 existing m41-token-capture tests still pass unchanged |
559
+ | REQ-M44-D7-02 | `metrics-schema-contract.md` bumped v2 → v2.1.0 documenting `cw_id` field + derivation rules (unattended: `cw_id == spawn_id`; in-session: `session_id + ":" + compaction_index`) | m44-d7-per-cw-attribution | **complete (2026-04-22)** |
560
+ | REQ-M44-D7-03 | `compaction-events-contract.md` bumped v1.0.0 → v1.1.0 adding `compaction_post_spawn` calibration event type appended to the same sink (`compactions.jsonl`); pairs `estimatedCwPct` with `actualCwPct` | m44-d7-per-cw-attribution | **complete (2026-04-22)** |
561
+ | REQ-M44-D7-04 | `scripts/gsd-t-calibration-hook.js` SessionStart hook: on `source=compact`, correlates with active spawn from `.gsd-t/.unattended/state.json`, derives `actualCwPct` from `payload.input_tokens` ÷ CW ceiling, appends one calibration row. Silent no-op when no active spawn (supervisor not running). | m44-d7-per-cw-attribution | **complete (2026-04-22)** — 19/19 unit tests in `test/m44-cw-attribution.test.js` |
562
+ | REQ-M44-D7-05 | Calibration hook safe to register alongside `scripts/gsd-t-compact-detector.js` — both fire on SessionStart, both write to the same sink, neither reads/writes the other's rows. Hook always exits 0 (throwing breaks Claude Code session start). | m44-d7-per-cw-attribution | **complete (2026-04-22)** — coexistence test verifies v1.0.0 detector rows remain intact when calibration row appended |
563
+ | REQ-M44-D7-06 | Backward compatibility: every pre-D7 v2 row remains a valid v2.1.0 row. Pre-D7 callers (no `cw_id` supplied) produce byte-identical output. Historical `token-usage.jsonl` rows are NOT backfilled with `cw_id`. | m44-d7-per-cw-attribution | **complete (2026-04-22)** |
564
+
565
+ Supporting contracts:
566
+ - `metrics-schema-contract.md` v2.1.0 — adds optional `cw_id` field; documents producer ownership (M44 D7) and derivation rules
567
+ - `compaction-events-contract.md` v1.1.0 — adds `compaction_post_spawn` calibration event; documents the calibration hook's lifecycle and guardrails
568
+
569
+ Out of scope for D7:
570
+ - Backfilling historical `token-usage.jsonl` rows with `cw_id` (consumers fall back to per-iter median for pre-D7 rows)
571
+ - Modifying `scripts/gsd-t-compact-detector.js` (D7 adds a companion hook only)
572
+ - Any economics logic (D6) or parallel dispatch logic (D2)
573
+
574
+ ## M44 Pre-Spawn Economics Estimator (D6 — Wave 2 gate, 2026-04-22)
575
+
576
+ Milestone 44 D6 ships the pre-spawn economics estimator — a per-task CW-footprint predictor that feeds the D2 parallel-CLI's gating math with a `{estimatedCwPct, parallelOk, split, workerCount, matchedRows, confidence}` decision. D6 is a **HINT**, never a veto: it produces a mode-specific recommendation; D2 owns the final gate. Mode-aware: `estimatedCwPct` is mode-agnostic, but `parallelOk` uses 85 % CW for in-session (orchestrator-CW headroom) and 60 % for unattended (per-worker CW); unattended-only `split=true` when `estimatedCwPct > 60`.
577
+
578
+ | REQ-ID | Requirement Summary | Domain | Status |
579
+ |--------|---------------------|--------|--------|
580
+ | REQ-M44-D6-01 | `bin/gsd-t-economics.cjs` exports `estimateTaskFootprint({taskNode, mode, projectDir})` → `{estimatedCwPct, parallelOk, split, workerCount, matchedRows, confidence}`. Synchronous. Never returns `undefined` (global-median fallback guarantees a number). | m44-d6-pre-spawn-economics | **complete (2026-04-22)** — 9/9 unit tests pass |
581
+ | REQ-M44-D6-02 | Three-tier corpus lookup: exact `command+step+domain` triplet (HIGH ≥5 rows, MEDIUM 1–4 rows), fuzzy (domain-only, then command-only) → LOW, global median → FALLBACK. Corpus loaded ONCE per `projectDir` (sync cached read), never re-read per call. | m44-d6-pre-spawn-economics | **complete (2026-04-22)** — all four tiers covered by tmpdir-fixture tests |
582
+ | REQ-M44-D6-03 | Mode-specific gates: in-session `parallelOk = estimatedCwPct ≤ 85`, unattended `parallelOk = estimatedCwPct ≤ 60` + `split = estimatedCwPct > 60`. `split` is always `false` for in-session. CW ceiling = 200 K tokens (matches `token-budget.cjs` / `context-meter-config.cjs` / `runway-estimator.cjs`). | m44-d6-pre-spawn-economics | **complete (2026-04-22)** — both mode thresholds asserted under and over the boundary |
583
+ | REQ-M44-D6-04 | Every call appends `{type:'economics_decision', ts, task_id, mode, estimatedCwPct, parallelOk, split, confidence, matchedRows}` to `.gsd-t/events/YYYY-MM-DD.jsonl`. Best-effort (silent on FS failure — never fails the estimate). | m44-d6-pre-spawn-economics | **complete (2026-04-22)** — event shape asserted in tests |
584
+ | REQ-M44-D6-05 | Calibrated against the live 528-row `token-usage.jsonl` corpus. Per-tier MAE (% of CW ceiling): HIGH 12.89 %, MEDIUM 0.00 % (small-n tautology), LOW 13.08 %, FALLBACK 15.06 %. Known-failure modes documented in the contract (§10). | m44-d6-pre-spawn-economics | **complete (2026-04-22)** — contract v1.0.0 documents measured numbers |
585
+ | REQ-M44-D6-06 | Zero external runtime deps (Node built-ins only). D6 is a HINT — D2 owns the final gate decision. | m44-d6-pre-spawn-economics | **complete (2026-04-22)** |
586
+
587
+ Supporting contract:
588
+ - `economics-estimator-contract.md` v1.0.0 — locks estimator interface, confidence tiers, mode-specific thresholds, event schema, known-failure modes, and calibration numbers. Downstream D2 may wire in.
589
+
590
+ Out of scope for D6:
591
+ - CW headroom arithmetic (D2 owns `computeInSessionHeadroom` / `computeUnattendedGate`)
592
+ - Dep-graph validation (D4) and file disjointness (D5)
593
+ - Writing back to `token-usage.jsonl` (read-only)
594
+ - Multi-iter task slicing (D6 recommends `split=true`; the caller plans the iter breakdown)
595
+
596
+ ## M44 Parallel CLI (D2 — Wave 3 integration, 2026-04-23)
597
+
598
+ Milestone 44 D2 ships the `gsd-t parallel` subcommand: a CLI wrapping the M40 orchestrator with task-level (not just domain-level) parallelism and mode-aware gating math. D2 consumes D1 (DAG), D4 (depgraph validation), D5 (disjointness prover), and D6 (economics estimator) and produces a validated worker plan. Extends — does not replace — the M40 orchestrator.
599
+
600
+ | REQ-ID | Requirement Summary | Domain | Status |
601
+ |--------|---------------------|--------|--------|
602
+ | REQ-M44-D2-01 | `bin/gsd-t-parallel.cjs` exports `runParallel({projectDir, mode, milestone, domain, dryRun})` and `runCli(argv, env)`. Node built-ins only; zero external runtime deps. | m44-d2-parallel-cli | **complete (2026-04-23)** |
603
+ | REQ-M44-D2-02 | `gsd-t parallel --help` prints usage (flags + gates + modes + contract reference) and exits 0 without side effects. | m44-d2-parallel-cli | **complete (2026-04-23)** |
604
+ | REQ-M44-D2-03 | `--mode in-session\|unattended` flag; auto-detect fallback: `GSD_T_UNATTENDED=1` → `unattended`, else `in-session`. Explicit `--mode` overrides env. Additional flags: `--milestone Mxx`, `--domain <name>`, `--dry-run`. | m44-d2-parallel-cli | **complete (2026-04-23)** |
605
+ | REQ-M44-D2-04 | `bin/gsd-t-orchestrator-config.cjs` exports `computeInSessionHeadroom({ctxPct, workerCount, summarySize})` → `{ok, reducedCount}`. `ok=true` iff `ctxPct + workerCount × summarySize ≤ 85`; otherwise reduce N until it fits. Final floor is N=1 — NEVER returns `ok=false` (in-session must never throw pause/resume). | m44-d2-parallel-cli | **complete (2026-04-23)** |
606
+ | REQ-M44-D2-05 | `bin/gsd-t-orchestrator-config.cjs` exports `computeUnattendedGate({estimatedCwPct, threshold=60})` → `{ok, split}`. `split=true` when `estimatedCwPct > threshold`; caller MUST slice into multiple iters. Actual slicing is orchestrator responsibility. | m44-d2-parallel-cli | **complete (2026-04-23)** |
607
+ | REQ-M44-D2-06 | `runParallel` wires three gates in sequence BEFORE any fan-out: D4 depgraph → D5 disjointness → D6 economics. Any veto emits `gate_veto` event `{type, task_id, gate, reason, ts}` and the task drops from the parallel batch (decision="sequential" or "veto-deps"). | m44-d2-parallel-cli | **complete (2026-04-23)** |
608
+ | REQ-M44-D2-07 | In-session mode reads `ctxPct` from `bin/token-budget.cjs::getSessionStatus()`. On reduction (`reducedCount < workerCount`), emits `parallelism_reduced` event `{type, original_count, reduced_count, reason:'in_session_headroom', ts}`. | m44-d2-parallel-cli | **complete (2026-04-23)** |
609
+ | REQ-M44-D2-08 | Unattended mode calls D6 `estimateTaskFootprint` per task + `computeUnattendedGate`. On split, emits `task_split` event `{type, task_id, estimatedCwPct, ts}`. | m44-d2-parallel-cli | **complete (2026-04-23)** |
610
+ | REQ-M44-D2-09 | `--dry-run` prints a fixed-width 6-column plan table (`task_id`, `domain`, `estimated CW%`, `disjoint?`, `deps ok?`, `decision`) followed by total worker count and mode; exits without spawning any workers. | m44-d2-parallel-cli | **complete (2026-04-23)** |
611
+ | REQ-M44-D2-10 | `.gsd-t/contracts/wave-join-contract.md` bumped v1.0.0 → v1.1.0 with §Mode-Aware Gating Math documenting both thresholds, `reducedCount` fallback behavior, the three event schemas, and invariant preservation. | m44-d2-parallel-cli | **complete (2026-04-23)** |
612
+
613
+ Supporting contract:
614
+ - `wave-join-contract.md` v1.1.0 — M44 D2 addendum locks the gating math surface, thresholds, and event schemas.
615
+
616
+ Out of scope for D2:
617
+ - Command file wiring (`commands/gsd-t-execute.md`, `gsd-t-wave.md`, etc.) — that is D3
618
+ - Actual task slicing implementation (D2 only emits `task_split`; orchestrator executes)
619
+ - Replacing or rewriting `bin/gsd-t-orchestrator.js` — M44 builds on M40
620
+
621
+ ## M44 Command-File Integration (D3 — Wave 3, 2026-04-23)
622
+
623
+ Milestone 44 D3 wires the five primary GSD-T command files to the D2 `gsd-t parallel` CLI so task-level parallel dispatch becomes available from the standard workflow. D3 is purely additive doc-ripple + integration blocks — no new library code, no new spawns. Existing sequential code paths remain intact; the parallel path is a conditional that falls back silently when any gate vetoes or when only a single task is pending.
624
+
625
+ | REQ-ID | Requirement Summary | Domain | Status |
626
+ |--------|---------------------|--------|--------|
627
+ | REQ-M44-D3-01 | `commands/gsd-t-execute.md` Step 3 gains an "Optional — Parallel Dispatch (M44)" block documenting the >1-pending-task + D4/D5/D6 gate conditional, `GSD_T_UNATTENDED` mode auto-detection (no hardcoded `--mode`), silent fallback to sequential, D2-owned observability, unattended zero-compaction invariant, and in-session never-interrupt invariant. | m44-d3-command-file-integration | **complete (2026-04-23)** |
628
+ | REQ-M44-D3-02 | `commands/gsd-t-wave.md` EXECUTE phase (Step 3 Phase Orchestration Loop) documents that the spawned execute agent owns the parallel-vs-sequential decision internally; wave orchestrator inherits `GSD_T_UNATTENDED` and does not configure mode. | m44-d3-command-file-integration | **complete (2026-04-23)** |
629
+ | REQ-M44-D3-03 | `commands/gsd-t-integrate.md` Step 3 (Wire Integration Points) gains a conditional block triggering only when integrating >1 domain simultaneously; single-domain wiring is unchanged. | m44-d3-command-file-integration | **complete (2026-04-23)** |
630
+ | REQ-M44-D3-04 | `commands/gsd-t-quick.md` Step 3 (Execute) gains a lightweight conditional block — no-op for single-task quick invocations (the common case); triggers only when >1 pending task AND all gates pass. | m44-d3-command-file-integration | **complete (2026-04-23)** |
631
+ | REQ-M44-D3-05 | `commands/gsd-t-debug.md` Step 3 (Debug Solo or Team) gains a conditional block triggering only for multi-domain contract-boundary/gap debug sessions; single-domain debug runs unchanged. | m44-d3-command-file-integration | **complete (2026-04-23)** |
632
+ | REQ-M44-D3-06 | `commands/gsd-t-help.md` documents the `gsd-t parallel` CLI entry and detailed command block mirroring the style of adjacent entries (flags, reads/writes, contract reference). | m44-d3-command-file-integration | **complete (2026-04-23)** |
633
+ | REQ-M44-D3-07 | `docs/GSD-T-README.md` commands table reflects M44 D3 parallel-dispatch behavior in the rows for execute, wave, quick, debug, and integrate (1-line note per command). | m44-d3-command-file-integration | **complete (2026-04-23)** |
634
+ | REQ-M44-D3-08 | `README.md` Workflow Phases table mentions task-level parallelism via `gsd-t parallel --help` in the Execute row. | m44-d3-command-file-integration | **complete (2026-04-23)** |
635
+ | REQ-M44-D3-09 | No command file hardcodes `--mode` — every integration block explicitly states that mode is auto-detected from `GSD_T_UNATTENDED=1`. No `--in-session` opt-out flag exists in any of the 5 command files. | m44-d3-command-file-integration | **complete (2026-04-23)** |
636
+ | REQ-M44-D3-10 | `docs/architecture.md` documents the parallel dispatch decision flow (command file → D2 `gsd-t parallel` → D4/D5/D6 gates → M40 orchestrator → workers) as a single authoritative diagram. | m44-d3-command-file-integration | **complete (2026-04-23)** |
637
+
638
+ Supporting contract:
639
+ - `wave-join-contract.md` v1.1.0 — the D2 CLI surface referenced by every D3 integration block.
640
+
641
+ Out of scope for D3:
642
+ - The parallel execution logic (D2 owns it)
643
+ - Any gate logic (D4 depgraph, D5 file-disjointness, D6 economics)
644
+ - Modifying `bin/gsd-t.js` or any other CLI router (D2-owned)
645
+ - Creating new test files — command files are validated by use per project CLAUDE.md conventions
646
+ - Modifying the commands' non-parallel code paths — integration blocks are ADDITIVE only
647
+
648
+ ## M44 Spawn Plan Visibility (D8 — Wave 3 observability, 2026-04-23)
649
+
650
+ Milestone 44 D8 delivers a right-side two-layer task panel in the dashboard and transcript visualizer. Layer 1 surfaces the full project/milestone task plan; Layer 2 scopes down to the currently-active spawn. Done tasks display a compact token cell (`in=Nk out=Nk $X.XX`). A post-commit git hook flips task status to `done` and performs token attribution by scanning `.gsd-t/token-log.md` rows within the spawn's time window. One protocol, three writers (`captureSpawn`, `autoSpawnHeadless`, unattended worker resume Step 0), one reader (dashboard SSE + transcript HTML panel).
651
+
652
+ | REQ-ID | Requirement Summary | Domain | Status |
653
+ |--------|---------------------|--------|--------|
654
+ | REQ-M44-D8-01 | `bin/spawn-plan-writer.cjs` exports `writeSpawnPlan({spawnId, kind, milestone, wave, domains, tasks, projectDir})` with atomic temp+rename writes into `.gsd-t/spawns/{spawnId}.json`. | m44-d8-spawn-plan-visibility | **complete (2026-04-23)** |
655
+ | REQ-M44-D8-02 | `bin/spawn-plan-status-updater.cjs` exports `markTaskDone({spawnId, taskId, commit, tokens?, projectDir})` and `markSpawnEnded({spawnId, endedReason, projectDir})` with atomic rewrites. | m44-d8-spawn-plan-visibility | **complete (2026-04-23)** |
656
+ | REQ-M44-D8-03 | Post-commit hook (`scripts/gsd-t-post-commit-spawn-plan.sh` + template `templates/hooks/post-commit-spawn-plan.sh`) greps commit messages for `[M\d+-D\d+-T\d+]` matches and flips matching tasks in every active spawn plan (where `endedAt === null`). Silent-fail (exit 0) on any error. | m44-d8-spawn-plan-visibility | **complete (2026-04-23)** |
657
+ | REQ-M44-D8-04 | Token attribution: hook parses `.gsd-t/token-log.md` rows where `Task` column matches the task id AND `Datetime-start >= spawn.startedAt`, sums `in/out/cr/cc/cost_usd`, writes to the task's `tokens` field. Null renders as `—` per the "zero is a measurement, dash is a gap" rule. | m44-d8-spawn-plan-visibility | **complete (2026-04-23)** |
658
+ | REQ-M44-D8-05 | Writer integration at 3 chokepoints — `bin/gsd-t-token-capture.cjs` `captureSpawn` calls `writeSpawnPlan` before `spawnFn()` and `markSpawnEnded` after (success + error both); `bin/headless-auto-spawn.cjs` before child launch; `commands/gsd-t-resume.md` Step 0 under `GSD_T_UNATTENDED_WORKER=1`. All try/catch-wrapped — writer failure never blocks the spawn. | m44-d8-spawn-plan-visibility | **complete (2026-04-23)** |
659
+ | REQ-M44-D8-06 | `bin/spawn-plan-derive.cjs` exports `derivePlanFromPartition({projectDir, milestone, currentIter})` that reads `.gsd-t/partition.md` + `.gsd-t/domains/*/tasks.md` and returns the `{wave, domains, tasks}` slice for the current incomplete-tasks wave. | m44-d8-spawn-plan-visibility | **complete (2026-04-23)** |
660
+ | REQ-M44-D8-07 | Dashboard server gains `GET /api/spawn-plans` (returns array of plan files where `endedAt === null`) and a `spawn-plan-update` SSE channel (fs.watch on `.gsd-t/spawns/*.json` with mtime-deduplicated emits). Additive — existing endpoints and SSE channels unchanged. | m44-d8-spawn-plan-visibility | **complete (2026-04-23)** |
661
+ | REQ-M44-D8-08 | `scripts/gsd-t-transcript.html` gains a right-side `<aside class="spawn-panel">` with two `<section>` layers, status icons `☐ ◐ ✓` (only one `◐` per spawn at a time), and the `fmtTokens({in,out,cr,cc,cost_usd})` renderer producing `in=12.5k out=1.7k $0.42` with k-suffix above 1000 and 2-decimal USD. Cumulative totals computed at milestone and spawn scope. | m44-d8-spawn-plan-visibility | **complete (2026-04-23)** |
662
+ | REQ-M44-D8-09 | No new LLM token cost — all writers derive plans deterministically from partition.md + tasks.md; reader is browser-only; status updater is shell + node. | m44-d8-spawn-plan-visibility | **complete (2026-04-23)** |
663
+ | REQ-M44-D8-10 | 5 test suites covering writer, status updater, post-commit hook (including token-log attribution), dashboard endpoint + SSE, and transcript renderer panel — 36 tests, all passing. | m44-d8-spawn-plan-visibility | **complete (2026-04-23)** |
664
+
665
+ Supporting contract:
666
+ - `.gsd-t/contracts/spawn-plan-contract.md` v1.0.0 — schema + writer/reader/updater protocol + silent-fail rules.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "3.16.12",
3
+ "version": "3.18.11",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-calibration-hook.js
4
+ *
5
+ * SessionStart hook that records a calibration event whenever Claude Code
6
+ * fires `source=compact` AND we can correlate the compaction with an
7
+ * active unattended spawn.
8
+ *
9
+ * The output is one NDJSON row appended to
10
+ * `<cwd>/.gsd-t/metrics/compactions.jsonl`, alongside the rows produced by
11
+ * the v1.0.0 detector (`scripts/gsd-t-compact-detector.js`). The two hooks
12
+ * are independent listeners on the same stdin payload and do not interact.
13
+ *
14
+ * { "type": "compaction_post_spawn", "schemaVersion": 1,
15
+ * "ts": ..., "cw_id": ..., "task_id": ..., "spawn_id": ...,
16
+ * "estimatedCwPct": ..., "actualCwPct": ... }
17
+ *
18
+ * The pairing of `estimatedCwPct` (D6's pre-spawn prediction) with
19
+ * `actualCwPct` (derived from the live compaction event) is what makes
20
+ * the calibration loop actionable — D6's estimator can self-correct from
21
+ * the delta.
22
+ *
23
+ * Behavior:
24
+ * - Zero-dep. Reads stdin JSON, silently fails on any error. Always exits
25
+ * 0 — throwing here would break Claude Code session startup.
26
+ * - Only acts when `payload.source === "compact"`.
27
+ * - Silently no-ops when `<cwd>/.gsd-t/.unattended/state.json` is missing,
28
+ * unparseable, not running, or carries no spawn correlation. The
29
+ * supervisor may not be running when a manual session compacts —
30
+ * accepted.
31
+ * - Safe to register alongside `gsd-t-compact-detector.js`. Both fire on
32
+ * SessionStart, both read the same payload, both write to the same
33
+ * sink, and neither reads or writes the other's rows.
34
+ *
35
+ * Contract: .gsd-t/contracts/compaction-events-contract.md (v1.1.0)
36
+ */
37
+ "use strict";
38
+
39
+ const fs = require("fs");
40
+ const path = require("path");
41
+
42
+ const MAX_STDIN = 1024 * 1024; // 1 MiB
43
+ const SCHEMA_VERSION = 1;
44
+ const DEFAULT_CW_CEILING_TOKENS = 200000; // input-token budget per CW
45
+
46
+ if (require.main === module) {
47
+ let input = "";
48
+ let aborted = false;
49
+
50
+ process.stdin.setEncoding("utf8");
51
+ process.stdin.on("data", (chunk) => {
52
+ input += chunk;
53
+ if (input.length > MAX_STDIN) {
54
+ aborted = true;
55
+ try { process.stdin.destroy(); } catch { /* noop */ }
56
+ }
57
+ });
58
+ process.stdin.on("error", () => { /* silent */ });
59
+ process.stdin.on("end", () => {
60
+ if (aborted) { exitClean(); return; }
61
+ try {
62
+ handle(input);
63
+ } catch {
64
+ // silent — never throw from a SessionStart hook
65
+ }
66
+ exitClean();
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Handle a single SessionStart payload. Pure-ish: I/O is via fs but no
72
+ * argument is mutated. Exported for testing.
73
+ *
74
+ * @param {string} rawInput raw stdin contents (UTF-8 JSON or empty)
75
+ * @param {object} [overrides] test injection: { cwd, now }
76
+ * @returns {object|null} the row that was written, or null if no-op
77
+ */
78
+ function handle(rawInput, overrides = {}) {
79
+ let payload;
80
+ try {
81
+ payload = JSON.parse(rawInput);
82
+ } catch {
83
+ return null;
84
+ }
85
+ if (!payload || typeof payload !== "object") return null;
86
+ if (payload.source !== "compact") return null;
87
+
88
+ const cwd = resolveCwd(payload, overrides);
89
+ if (!cwd) return null;
90
+
91
+ const gsdDir = path.join(cwd, ".gsd-t");
92
+ if (!fs.existsSync(gsdDir)) return null;
93
+
94
+ const correlation = readActiveSpawn(cwd);
95
+ if (!correlation) return null; // silent no-op when no active spawn
96
+
97
+ const actualCwPct = deriveActualCwPct(payload, correlation.cwCeilingTokens);
98
+ if (actualCwPct == null) return null;
99
+
100
+ const metricsDir = path.join(gsdDir, "metrics");
101
+ const outPath = path.join(metricsDir, "compactions.jsonl");
102
+
103
+ const resolvedOut = path.resolve(outPath);
104
+ const resolvedMetrics = path.resolve(metricsDir) + path.sep;
105
+ if (!resolvedOut.startsWith(resolvedMetrics)) return null;
106
+
107
+ try {
108
+ fs.mkdirSync(metricsDir, { recursive: true });
109
+ } catch {
110
+ return null;
111
+ }
112
+
113
+ const now = (overrides.now instanceof Date) ? overrides.now : new Date();
114
+ const row = {
115
+ type: "compaction_post_spawn",
116
+ schemaVersion: SCHEMA_VERSION,
117
+ ts: now.toISOString(),
118
+ cw_id: correlation.cw_id,
119
+ task_id: correlation.task_id,
120
+ spawn_id: correlation.spawn_id,
121
+ estimatedCwPct: correlation.estimatedCwPct,
122
+ actualCwPct,
123
+ };
124
+
125
+ fs.appendFileSync(outPath, JSON.stringify(row) + "\n", "utf8");
126
+ return row;
127
+ }
128
+
129
+ /**
130
+ * Resolve a usable cwd from the payload, mirroring detector semantics:
131
+ * absolute string → use as-is; missing/null → fall back to process.cwd();
132
+ * any other shape → no-op (return null).
133
+ */
134
+ function resolveCwd(payload, overrides) {
135
+ if (overrides && typeof overrides.cwd === "string") return overrides.cwd;
136
+ if (typeof payload.cwd === "string") {
137
+ if (!path.isAbsolute(payload.cwd)) return null;
138
+ return payload.cwd;
139
+ }
140
+ if (payload.cwd === undefined || payload.cwd === null) return process.cwd();
141
+ return null;
142
+ }
143
+
144
+ /**
145
+ * Read the supervisor's view of the currently-active spawn from
146
+ * `.gsd-t/.unattended/state.json`. Returns null when:
147
+ * - the file is missing / unreadable / not JSON
148
+ * - state.status is not "running"
149
+ * - we cannot derive a spawn_id (no sessionId)
150
+ *
151
+ * The fields we expose:
152
+ * - cw_id : per-CW attribution key (== spawn_id for unattended workers)
153
+ * - task_id : active task identifier, if the supervisor recorded one
154
+ * - spawn_id: stable spawn identifier (sessionId for unattended)
155
+ * - estimatedCwPct: D6's pre-spawn prediction (if supervisor recorded it)
156
+ * - cwCeilingTokens: optional override of the default ceiling
157
+ */
158
+ function readActiveSpawn(cwd) {
159
+ const statePath = path.join(cwd, ".gsd-t", ".unattended", "state.json");
160
+ let raw;
161
+ try {
162
+ raw = fs.readFileSync(statePath, "utf8");
163
+ } catch {
164
+ return null;
165
+ }
166
+ let state;
167
+ try {
168
+ state = JSON.parse(raw);
169
+ } catch {
170
+ return null;
171
+ }
172
+ if (!state || typeof state !== "object") return null;
173
+ if (state.status !== "running") return null;
174
+
175
+ const spawn_id =
176
+ (typeof state.activeSpawnId === "string" && state.activeSpawnId) ||
177
+ (typeof state.spawn_id === "string" && state.spawn_id) ||
178
+ (typeof state.sessionId === "string" && state.sessionId) ||
179
+ null;
180
+ if (!spawn_id) return null;
181
+
182
+ const cw_id =
183
+ (typeof state.cw_id === "string" && state.cw_id) ||
184
+ (typeof state.activeCwId === "string" && state.activeCwId) ||
185
+ spawn_id; // unattended: one spawn = one CW
186
+
187
+ const task_id =
188
+ (typeof state.activeTask === "string" && state.activeTask) ||
189
+ (typeof state.task_id === "string" && state.task_id) ||
190
+ (typeof state.currentTask === "string" && state.currentTask) ||
191
+ null;
192
+
193
+ let estimatedCwPct = null;
194
+ for (const key of ["estimatedCwPct", "estimated_cw_pct", "predictedCwPct"]) {
195
+ if (typeof state[key] === "number" && Number.isFinite(state[key])) {
196
+ estimatedCwPct = state[key];
197
+ break;
198
+ }
199
+ }
200
+
201
+ let cwCeilingTokens = DEFAULT_CW_CEILING_TOKENS;
202
+ for (const key of ["cwCeilingTokens", "cw_ceiling_tokens", "cwCeiling"]) {
203
+ if (typeof state[key] === "number" && state[key] > 0) {
204
+ cwCeilingTokens = state[key];
205
+ break;
206
+ }
207
+ }
208
+
209
+ return { cw_id, task_id, spawn_id, estimatedCwPct, cwCeilingTokens };
210
+ }
211
+
212
+ /**
213
+ * Derive actualCwPct (0.0–2.0) from the compaction payload. Looks at
214
+ * `payload.input_tokens`, then falls back to nested fields used by the
215
+ * scanner's compactMetadata shape. Returns null when nothing usable.
216
+ */
217
+ function deriveActualCwPct(payload, cwCeilingTokens) {
218
+ const ceiling = (typeof cwCeilingTokens === "number" && cwCeilingTokens > 0)
219
+ ? cwCeilingTokens
220
+ : DEFAULT_CW_CEILING_TOKENS;
221
+
222
+ let inputTokens = null;
223
+ if (typeof payload.input_tokens === "number" && payload.input_tokens >= 0) {
224
+ inputTokens = payload.input_tokens;
225
+ } else if (typeof payload.preTokens === "number" && payload.preTokens >= 0) {
226
+ inputTokens = payload.preTokens;
227
+ } else if (payload.compactMetadata && typeof payload.compactMetadata === "object") {
228
+ const m = payload.compactMetadata;
229
+ if (typeof m.preTokens === "number" && m.preTokens >= 0) {
230
+ inputTokens = m.preTokens;
231
+ }
232
+ }
233
+ if (inputTokens == null) return null;
234
+
235
+ const pct = inputTokens / ceiling;
236
+ // Clamp: keep within a sane band. We allow >1.0 (ceiling can be lower
237
+ // than the actual hard limit) but cap at 2.0 to avoid runaway values.
238
+ if (!Number.isFinite(pct) || pct < 0) return 0;
239
+ if (pct > 2) return 2;
240
+ return pct;
241
+ }
242
+
243
+ function exitClean() {
244
+ try { process.stdout.write(""); } catch { /* noop */ }
245
+ process.exit(0);
246
+ }
247
+
248
+ module.exports = {
249
+ handle,
250
+ // Exported for white-box testing only:
251
+ _readActiveSpawn: readActiveSpawn,
252
+ _deriveActualCwPct: deriveActualCwPct,
253
+ _resolveCwd: resolveCwd,
254
+ SCHEMA_VERSION,
255
+ DEFAULT_CW_CEILING_TOKENS,
256
+ };