Conversation
…tion
Closes the budget cluster: snapshot-burn (slice 14) +
daily-cost-report (slice 18) + project-runway (this slice) are
now all TS. Once this lands, daily-cost-report.ts can switch
from spawning project-runway.sh to project-runway.ts.
Behavioural improvements over bash original (deliberate, not drift):
- File-existence check uses statSync().isFile() + try/catch
rather than existsSync — bash `-f` rejects directories,
existsSync accepts them (slice-18 mirror).
- JSONL parsing is native (readFileSync + split + JSON.parse)
rather than per-line jq spawn-out — projection script reads
already-persisted JSON, so jq is a heavy dependency for what
is structurally a typed reduce. snapshot-burn.ts still needs
gh api for capture; this is projection only.
- requireInt validation matches bash `case '$val' in
''|*[!0-9]*) ...` with TS `requireInt(flag, val)` returning
`number | ArgError` — same exit code 2, same error wording
(Codex P2 NM59qF00 + NM59qH2H, Copilot P1 NM59qGJ- on the
bash original).
Byte-equivalence verified on this repo state:
diff <(bun tools/budget/project-runway.ts) \
<(./tools/budget/project-runway.sh) # empty
diff <(bun tools/budget/project-runway.ts --json) \
<(./tools/budget/project-runway.sh --json) # empty
Error paths verified equivalent: --stages abc → exit 2 with
matching message; --file <missing> → exit 1; --bogus → exit 2.
Tools used: tsc --noEmit clean; eslint clean per the existing
tsc-tools CI gate (#890).
Composes with:
- tools/budget/snapshot-burn.ts (slice 14, #894)
- tools/budget/daily-cost-report.ts (slice 18, #901)
- docs/trajectories/typescript-bun-migration/RESUME.md
- docs/trajectories/typescript-bun-migration/slice-audits.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Ports the budget “projection” primitive from bash to TypeScript/Bun so the budget cluster can be fully TS-based (reading docs/budget-history/snapshots.jsonl and emitting text/JSON runway projections).
Changes:
- Added
tools/budget/project-runway.ts(TS/Bun port ofproject-runway.sh) with native JSONL parsing and CLI validation. - Appended Slice 19 audit notes to the TS/Bun migration trajectory.
- Updated the TS/Bun migration RESUME dashboard to reflect slice progress.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tools/budget/project-runway.ts | New TS/Bun implementation of the runway projection script (CLI parsing, JSONL parsing, projection + emitters). |
| docs/trajectories/typescript-bun-migration/slice-audits.md | Adds Slice 19 audit entry describing the port and equivalence checks. |
| docs/trajectories/typescript-bun-migration/RESUME.md | Updates trajectory status/milestone text for slice 19 in-flight. |
| if (a === "--stages") return applyIntFlag("--stages", next, (n) => { state.stages = n; }); | ||
| if (a === "--copilot-rate") return applyIntFlag("--copilot-rate", next, (n) => { state.copilotRate = n; }); | ||
| if (a === "--actions-free-ms") return applyIntFlag("--actions-free-ms", next, (n) => { state.actionsFreeMs = n; }); |
| // Match bash `wc -l` semantics over JSONL: count non-empty lines. | ||
| // Empty trailing newline shouldn't inflate the count. | ||
| return raw.split("\n").filter((line) => line.length > 0); |
|
|
||
| ### Code-pattern audit (per-port) | ||
|
|
||
| - **`project-runway.ts`** (297 → 430 lines): JSONL parsing entirely native (`readFileSync` + split + `JSON.parse`) — no jq spawn-out, since this is a pure data-projection script (snapshot-burn.ts still needs `gh api` for capture, but projection is over already-persisted JSON). Argument parsing splits flag classification (`classifyFlag`) from int-flag application (`applyIntFlag`) so each function stays under cognitive-complexity 15. Validation mirror: bash `case '$val' in ''|*[!0-9]*) ...` → TS `requireInt(flag, val)` returning `number | ArgError` discriminated-union — same semantics (non-empty + digits-only), same exit code 2, same error-message wording. The jq path expressions like `([.repos[].agg.total_duration_ms // 0] | add) // 0` map cleanly to typed `RepoEntryLike[].map(...).reduce(...)` once you express the snapshot shape as `SnapshotLike` with optional fields throughout (`?:`). Default-zero (`?? 0`) matches jq `// 0` per-element + `add // 0` empty-array semantics. |
…sure (#903) Per the consolidated-row-pattern (rows from 03:41Z + 05:01Z arc closures): when a session lands many small commits across multiple ticks, a single consolidated row summarizing the arc is more signal-dense than N minimal rows. Covers the slices 15-19 arc that landed after the 05:43Z slice-14 row: - #896 slice 15 (grok.ts) — peer-call cluster opens - #898 slice 16 (gemini.ts) — peer-call sibling - #899 backport to grok from #898 review-cycle findings - #900 slice 17 (codex.ts) — peer-call cluster closes - #897 B-0107 row — CodeQL dismissal pattern - #901 slice 18 (daily-cost-report.ts) — budget wrapper - #902 slice 19 (project-runway.ts) — budget cluster closes (in flight) Three new substrate observations recorded for future-Otto: - sibling-port-cost decreases monotonically (round-2/3 fixes bake into later siblings proactively) - kernel-pipe vs JS-space stream interleaving distinction (bash `2>&1` merges shell-side; `result.stdout + result.stderr` in JS does not preserve chronological ordering) - fix-the-bug + file-the-row + implement-the-row + closeout pattern is the durable shape (B-0106 + B-0107 are both worked examples) Cron 98fc7424 still armed. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 319e38521a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const repos = obj.repos ?? []; | ||
| const totalMs = sumOptional(repos.map((r) => r.agg?.total_duration_ms)); |
There was a problem hiding this comment.
Reject malformed snapshot rows instead of defaulting to zero
This parser silently treats missing/non-object repos as an empty array, so corrupted JSONL rows can pass as valid and produce zeroed totals instead of failing. In the previous shell version, equivalent jq queries (.repos[]) error on missing/null repos, which correctly surfaces malformed history files; here the script can emit a plausible-looking projection from bad input and mislead budgeting decisions. Please validate the snapshot shape (at least require repos to be an array) and return the documented malformed-file exit path when that contract is violated.
Useful? React with 👍 / 👎.
#907) Last git-cluster port: tools/git/batch-resolve-pr-threads.{sh→ts}. Slice 13 (push-with-retry) + slice 20 (batch-resolve-pr-threads) together complete the git-cluster. Behavioural improvements over bash original (deliberate, not drift): - jq pipelines replaced with native JSON parse + array operations; drops jq from runtime deps (only `gh` required now). - Pattern classification via TypeScript pattern arrays + `.some()` rather than bash `for pat in ; do [[ ]]; done` — same semantics, more type-safe with `Classification = "dangling-ref" | "name-attribution" | "unknown"` discriminated union. - ResolveError discriminated record with `stage: "reply" | "resolve"` so failure mode is visible in error messages. Byte-equivalence verified on this repo state: - Three argument-validation paths byte-equivalent (no args / bad pr-number / unknown second arg) — same exit code + same message. - Live dry-run on PR #902 (4 unresolved threads) byte-equivalent — empty diff against bash original. - Apply mode not exercised (would mutate live PR state); code path verified by inspection. All bash safety rails from PR #199 (Copilot/Codex) preserved: positive-integer validation, exact-`--apply` second-arg check, GraphQL errors array inspection, null-pullRequest detection, paginated thread fetch (50 per page) + paginated per-thread comments (50 per thread with truncation warning), positional -F args (avoids parameter-expansion-quote pitfall), printf-not-echo for review-comment bodies (handled via TypeScript string operations). Bucket B 2 → 1 (only tools/pr-preservation/archive-pr.sh 674L remains; bash+Python mix — slice 21). Composes with: - tools/git/push-with-retry.ts (slice 13, #892) — git-cluster siblings - docs/trajectories/typescript-bun-migration/RESUME.md updated - docs/trajectories/typescript-bun-migration/slice-audits.md slice 20 audit Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…Bun migration (#908) * ts(B-0086): port 1 pr-preservation script (.sh→.ts) — slice 21 of TS/Bun migration Last Bucket B file: tools/pr-preservation/archive-pr.{sh→ts}. After this PR merges, **Bucket B is empty** — every file flagged for TS port has been ported. The trajectory transitions from "porting" phase to "soak + bash retirement" phase. Behavioural improvements over bash original (deliberate, not drift): - Drops Python from runtime deps entirely. Bash original was 217 lines bash + ~457 lines embedded Python (GraphQL fetcher + Markdown formatter); TS port collapses both into single Bun runtime. No more bash/Python boundary, no more mktemp + trap cleanup, no `set +e` to capture Python exit code. - Native JSON parsing replaces all jq/Python json shells. - Generic `paginateTopLevel<T>` helper with type-safe extractor handles the cursor loop for all 3 top-level connections (reviewThreads/reviews/comments). `paginateThreadComments` handles the per-thread case. - `detectFenceMarker` preserves CommonMark §4.5 fence rules strictly: leading-space-count ≤ 3 + no tab in prefix; closing fence same marker char + length ≥ opener. This matches the Python original's nuanced fence detection (Otto-241 etc). - `yamlQuote` post-processes `JSON.stringify` output to escape non-ASCII codepoints as \uXXXX, matching Python's `json.dumps(ensure_ascii=True)` wire-format default. Without this, titles with → / — would diverge from bash output. Byte-equivalence verified on this repo state: - 2 argument-validation paths byte-equivalent (no args / bad PR number) — same exit code 1 + same message. - Live archive run on PR #902 (4 threads, 2 reviews, 0 comments) byte-equivalent EXCEPT `archived_at` (timestamp) + `archive_tool` (.sh vs .ts — deliberate self-reference). Title with non-ASCII chars escapes correctly via the yamlQuote fix. All bash safety rails preserved: positive-integer PR validation, GH_REPO env-var preference + `gh repo view` fallback, 2/3-segment NWO parsing with Enterprise HOST validation (dot required), slash-injection defence on owner/name, paginated GraphQL fetch (top-level + per-thread), GraphQL `errors` array inspection, null-pullRequest detection, idempotent archive path via PR-<NNNN>-* glob (Otto-235), CommonMark §4.5 fence detection (Otto-241), trailing-newline-only rstrip (preserves two-space markdown hard-line-breaks). Bucket B 1 → 0. Bucket C: 2 (gh-api-heavy scripts pending maintainer decision). Bucket A: 14 (stays bash by design). Composes with: - tools/git/batch-resolve-pr-threads.ts (slice 20, #907) — same GraphQL pagination shape - docs/trajectories/typescript-bun-migration/RESUME.md updated - docs/trajectories/typescript-bun-migration/slice-audits.md slice 21 audit appended Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ts-bun-audits): MD038 — rephrase to avoid leading-space inside code spans * fix(ts-bun-audits): address #908 review threads — line counts + ensure_ascii framing + usage-line carve-out Round-1 fixes for PR #908 (4 Copilot P1 threads): - Line-count drift (Copilot P1): I claimed "674 → 590" but actual is "674 → 806". Updated and added a one-line note explaining why TS is larger than bash (explicit type interfaces replace Python's untyped dict navigation). - ensure_ascii=True framing was wrong (Copilot P1, twice): Python's json.dumps with ensure_ascii=True does NOT emit literal `→`/`—` — it emits \uXXXX escapes. My audit prose said "Python escapes to `→`" which contradicted both the byte-equivalence goal and what yamlQuote actually implements. Rewrote to clearly say Python emits \uXXXX form (e.g. → for right-arrow, — for em-dash) and the TS yamlQuote post-processes JSON.stringify to match. - Usage-line not byte-equivalent (Copilot P1, twice): bash echoes `$0` showing the actual `./tools/...sh` path; TS hard-codes `bun tools/...ts` so the user sees the form they should run. Reframed equivalence claim to be honest: same exit code + same error-body, but usage-line script-path is intentionally NOT byte-equivalent — same carve-out as the `archive_tool` YAML self-reference. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Slice 19 of the TS/Bun migration (B-0086). Ports
tools/budget/project-runway.sh(297 lines bash) →tools/budget/project-runway.ts(430 lines TS) — the projection layer overdocs/budget-history/snapshots.jsonl.Closes the budget cluster. Once this lands, all three primitives are TS:
snapshot-burndaily-cost-reportproject-runwaydaily-cost-report.tscan then switch from spawning the.shsiblings to spawning the.tsversions (follow-up slice).Behavioural improvements (deliberate, not drift)
statSync().isFile()overexistsSync()— bash-frejects directories,existsSyncaccepts them. Slice-18 mirror.readFileSync + split + JSON.parserather than per-linejqspawn-out. Projection reads already-persisted JSON;jqis overkill for a structurally typed reduce. (snapshot-burn.tsstill needsgh apifor capture; this is projection only.)requireIntvalidation — matches bashcase '$val' in ''|*[!0-9]*) ...exactly: same exit code 2, same wording (Codex P2 NM59qF00 + NM59qH2H, Copilot P1 NM59qGJ- on the bash original).Verification
Both modes byte-equivalent on this repo state (snapshots.jsonl with N=4).
Error-path equivalence sampled:
--stages abc--file /tmp/nonexistent--bogusbun --bun tsc --noEmit -p tsconfig.jsonclean (the newlint (tsc tools)gate from #890 will verify in CI).Test plan
lint (tsc tools)gate passesComposes with
tools/budget/snapshot-burn.ts(slice 14, ts(B-0086): port 1 budget script (.sh→.ts) — slice 14 of TS/Bun migration #894) — capture-side primitivetools/budget/daily-cost-report.ts(slice 18, ts(B-0086): port 1 budget script (.sh→.ts) — slice 18 of TS/Bun migration #901) — wrapper that spawns bothdocs/trajectories/typescript-bun-migration/RESUME.md— trajectory dashboard updateddocs/trajectories/typescript-bun-migration/slice-audits.md— slice 19 audit appended🤖 Generated with Claude Code