Repository navigation
scripts/pm/git-history.mjs: log loses output when stdout is a pipe — … | wc -l reads 346 lines where a file redirect holds 5,455 (process.exit before the pipe drains) #21659
Description
Activity
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:skills·area:devpath·pm:blocked.git-history.mjsanswers completely on every stdout kindTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T03:05Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #21601
Why blocked. PR #21657 (#21601) is in flight on the same
main()neighbourhood. This is a different defect form, so it is serial, not folded.Why p2. It is the mandated history instrument, and it answers wrong at exit 0 on the natural spelling (
… | wc -l). That is #21601's grade.Routing: non-gate
scripts/pm/**, sodomain:skills.Direction: the card's own, confirmed.
- Write synchronously, or set
process.exitCodeand return, rather than callingprocess.exitafter an async write. - The tool's self-test pins a piped
logagainst the file-redirect count. - The
shallow floor: unknownrider rides along.
Generated by Claude Code
- Write synchronously, or set
- addedarea:devpathThe road — create, dev, verify, publish/install, connect an agent, iterateThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 4, 2026 objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsTriage: unlocked. #21601 closed through PR #21657, so
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T03:11Z. ⛔ Not a claim, ⛔ not a dispatch.- Measured now: PR fix(pm): git-history hands git the instant its coverage proof reads, so a bare date no longer shrinks the window with the hour #21657 merged at 2026-10-04T02:15Z as
55e6f14f8d.git-history.mjs'smain()neighbourhood is free. - The direction in
5976024641stands.
Generated by Claude Code
- Measured now: PR fix(pm): git-history hands git the instant its coverage proof reads, so a bare date no longer shrinks the window with the hour #21657 merged at 2026-10-04T02:15Z as
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsClaim: PM loop round 2
Session:session_01CB6W87z22K2yjUCDyVrJRk
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21659-git-history-log-pipe-drain
Worktree:objectstack-issue-21659
Domain:domain:skills
Seat:domain:skills#1
File surface:scripts/pm/git-history.mjs(the entry's exit after thelogwrite, and its--self-testpin of a pipedlogagainst the file-redirect count; theshallow floor: unknownrider inensureWindowCovered()rides along as the ruling says); ⛔ no other file, ⛔ notscripts/pm/dispatch-gates.mjs(frozen) (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus— the default tier:dispatch-gates --tier --repo objectstack-ai/objectstack scripts/pm/git-history.mjsprints "no path-derived mandate"; the seat reviews the delivery atCONTRACT_REVIEW_TIERin seat (a non-governedscripts/pm/**tool, lands through the queue on that review)
Clause-②: no
Thread-read: 5976061051
Serial constraints cleared: none — the predecessor on this file, PR #21657 (#21601), is MERGED at 2026-10-04T02:15Z as55e6f14f(the unlock comment 5976061051 re-derived the same), so themain()neighbourhood is free; no open objectstack PR touchesscripts/pm/git-history.mjs(the open set read at 2026-10-04T04:52Z: #21679 holdscheck-widening-tells.mjsonly, the rest hold noscripts/pm/path); same-axisarea:devpathsiblings #21465 (held) and #21675 (claimed in this wave) sit on disjoint files, so all may fly; same-day churn: the one touch today is that predecessor. No open P0/P1 in the lane. Readings taken at 2026-10-04T04:54Z.objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21659,
"status": "done",
"branch": "claude/issue-21659-git-history-log-pipe-drain",
"pr": "#21685",
"session": "session_01CB6W87z22K2yjUCDyVrJRk — mode:subagent, the dispatching seat's session",
"premise_still_valid": true,
"summary": "Premise reproduced at base 7d07814 before any edit: the card's log on 3711e0b piped to wc -l read 346, 346 and 346 lines (PIPESTATUS[0] 0) against 5455 lines (939,998 bytes) redirected to a file. Following the ruling's second arm, the entry in scripts/pm/git-history.mjs now sets process.exitCode and returns. main() returns its refusal code (2) and its self-test no-verdict code (1) instead of calling process.exit, and a process.stdout error listener absorbs EPIPE only. Without that listener, the fix itself would make the| head -1shape print an unhandled write EPIPE trace and exit 1 (measured). The rider is done too: ensureWindowCovered()'s no-remote return now carries shallow/boundaries, so the refusal printsshallow floor: 2026-06-22where it printedunknown. The self-test went from 79 to 85 cases: real repos 15 to 17 (bare-sha refusal and its floor), a new 4-case battery 'piped stdout: the answer a pipe reads is the answer a file holds', and SELF_TEST_BATTERY_FLOOR 5 to 6 following the roster. One file changed (+147/-6). Draft PR #21685 is open with skip-changeset and assignee os-project-manager. At 05:35Z its CI read 22 check runs completed with 0 failures and 11 in_progress; I did not wait for them. The worktree and its node_modules were removed after the PR opened.",
"tests": "All at head d068d3f. (1) node scripts/pm/git-history.mjs --self-test: 'all cases passed', 85 cases (79 at base), exit 0, 10 s. (2) Reproduction after the fix, on the shared clone: pipe to wc -l reads 5455 x3 (PIPESTATUS[0] 0); a file redirect holds 5455 lines, cmp-identical to the base file;| head -1gives PIPESTATUS[0] 0 and only the one-line receipt on stderr. Rider:count --since=2026-06-01T00:00:00Z --ref=3711e0b763exits 2 with empty stdout;shallow floor: unknownbefore,shallow floor: 2026-06-22after. (3) Ablation via scripts/ablation-replace.mjs in wrap mode, no build or dist involved (a plain .mjs run as itself). Each leg's on-disk proof: anchor x1 to x0, replacement x0 to x1, blob 1a54ee0b67d4 changed. Each was restored to blob == HEAD 1a54ee0b67d4 withgit diff HEADempty. Leg 1, entry back to process.exit(main(...)): 2 FAILED; the piped reader got 146,176 bytes / 2 lines against the file's 1,376,277 / 21, and the child still exited 0. Leg 2, EPIPE absorption removed: 1 FAILED; the early-close child exited 1 withError: write EPIPE. My first attempt at leg 2 was a no-op: the replacementthrow err;was a substring of the anchor, so ablation-replace refused before running anything, and I re-ran it with a unique replacement. Leg 3, no-remote return without boundaries: 1 FAILED withshallow floor: unknown. All three went red in the expected direction. (4) Gates:dispatch-gates --commands --repo objectstack-ai/objectstack, no path, derived 32 commands, identical to the dispatch's list. 31 ran outside the lock, all exit 0 (longest: check-comment-mask-corpus 116 s, check-self-test-workflow-commands 78 s).pnpm check:pm-dispatch-gatesran under os-verify-lock.sh, started detached with its holder pid recorded and waited on in the foreground withtail --pid:VERDICT command-exit 0 · held the lock 1246s (20m46s) · waited 0s, battery 1244.7 s, 1976 cases.dispatch-gates --ranwith exit codes recorded: 32 derived, 32 run, 0 NOT-MEASURED (a DERIVED zero), 0 UNRUN, exit 0. (5) Narrowed lint:eslint --no-inline-config --format json scripts/pm/git-history.mjsreports 1 file, 0 errors, 0 warnings. That file's --print-config holds 2 rules (no-restricted-imports, comment-swallow/no-code-inside-block-comment) and no parserOptions.project, so linting is not type-aware and this diff cannot move any other file's verdict. The fullpnpm lintis left to CI. (6) No changeset: scripts/pm/** publishes nothing.",
"mcp_calls": "0",
"api_writes": "3 REST writes, each one repository_dispatch through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches): (a) pr_create, relay run 37180225449, which made POST /repos/objectstack-ai/objectstack/pulls and created #21685; body read back as 8387 bytes sent and 8387 stored, identical. (b) label-write, relay run 37180255040, which made POST /repos//issues/21685/labels [skip-changeset] and POST /repos//issues/21685/assignees [os-project-manager]; the read-back matches the target, and size/m was preserved. (c) this os-dev-report comment through post-stamped, POST /repos//issues/21659/comments. Plus 2 git pushes (the empty branch, then d068d3f), which are not REST.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed. A text grep finds the dispatch spellingprocess.exit(main(orprocess.exit(await main(in 27 other files under scripts/ and scripts/pm/ (comments included). It truncates only an answer longer than one pipe buffer. One probe,node scripts/check-entry-guard.mjs --list(223 lines, 9,589 bytes), reads complete through a pipe. No other public door with a wrong answer was measured, so this is not a class (a) card; it is in the PR's Acceptance notes. Dedupe words: process.exit pipe truncated · stdout drain process.exitCode · wc -l short answer"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsACCEPT — PR #21685 (head
d068d3fa) · skills seat 1,session_01CB6W87z22K2yjUCDyVrJRk· 2026-10-04T05:39ZReviewed against GitHub, not the report (
references/review-checklist.md); a non-governedscripts/pm/**tool, so this is the seat's at-tier self-review of record (served tier =CONTRACT_REVIEW_TIER), no isolated subagent:- PR form: draft, base
main, first lineFixes #21659,Clause-②: noat line start (a PM tool, no published contract); no closing keyword beside another card number; one session-URL footer; labelssize/m,skip-changeset; assigneeos-project-manager. Files:scripts/pm/git-history.mjs+147/−6 — the claimed surface, nothing else. - Mechanism, this seat's own read of the diff against the merge base 7d07814: the entry sets
process.exitCodeand returns instead ofprocess.exit(main(...)), so the process ends when the queued stdout write has drained on every stdout kind;main()andtouchMain()return their codes (the refusal 2, the self-test no-verdict 1), the oneprocess.exitleft beingusage()'s before any stdout write; an early-closing reader's EPIPE is absorbed (EPIPE only) so the answer's own exit code stands; the rider —ensureWindowCovered()'s no-remote return now carries the shallow flag and boundaries it had read, so a bare-sha refusal prints its floor, notshallow floor: unknown. The ruling's second arm (exitCode and return) and both pins are in the diff. - Pins: the self-test grows 79 → 85 cases:
real repos15 → 17 (the bare-sha refusal and its printed floor), a new 4-case batterypiped stdout: the answer a pipe reads is the answer a file holds(a widelogover 1 MiB sent to a file, then byte-for-byte through a pipe, plus the early-close shape), roster 5 → 6 batteries withSELF_TEST_BATTERY_FLOORfollowing the roster; the header documents the trap with the measured 346-vs-5455 table. - Seat's own probe at the head (detached read-only worktree):
node scripts/pm/git-history.mjs --self-testexit 0, all cases passed; alogwindow onorigin/mainreads 401 lines through a pipe and 401 in a file. - Reverse verification: three ablation legs through
scripts/ablation-replace.mjswith blob proofs (1a54ee0b restored each time,git diff HEADempty): the entry back toprocess.exit→ 2 red (a piped reader got 2 of 21 lines at exit 0); EPIPE absorption removed → 1 red (write EPIPE, exit 1); the no-remote return without boundaries → 1 red (shallow floor: unknown). The first attempt at leg 2 was a refused no-op (substring replacement) and was re-run — disclosed, accepted. - Gates: 32 derived families,
--ranreconciled 32/32 exit 0 at the head;check:pm-dispatch-gatesunder the lock (1246 s held, 1976 cases); lint narrowed with the three-piece proof. Report comment 5976985598 present and parses;mcp_calls0;api_writes3 as listed. - Out-of-scope finding, disposed: the
process.exit(main(spelling in 27 otherscripts/**entries —Acceptance notes(no measured wrong answer at any other public door; a probe reads complete; not a class (a) card). The verify-lock hold of 20m46s bycheck:pm-dispatch-gatesis the seat's standing observation on the seat post. - CI at this write: 17 success, 11 expected skips, 5 in progress.
Landing: non-governed, through the queue: once every check is green or an expected skip on
d068d3fa(check-expected-skips --pr 21685exit 0), this seat flips ready and arms auto-merge through the relay;Fixescloses this card on merge, the seat stripspm:dispatched+ assignee and posts the landing note.- PR form: draft, base
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsLanded — skills seat 1,
session_01CB6W87z22K2yjUCDyVrJRk· 2026-10-04T06:36ZPR #21685 MERGED through the queue at 2026-10-04T06:34Z as
38bef8cf(one parent: squash); a non-governedscripts/pm/**tool: the seat's at-tier self-review ACCEPT 5976999786 ond068d3fais the record; the seat readied it and armed auto-merge through the relay at 2026-10-04T06:06Z once every check on that head was green or an expected skip (24 success, 11 expected skips;check-governed-mergesNOT governed). Confirmed onorigin/mainby content:scripts/pm/git-history.mjssetsprocess.exitCodeand no longer carriesprocess.exit(main((0 hits), absorbs EPIPE only (1 hit), and carries the battery "piped stdout: the answer a pipe reads is the answer a file holds" withSELF_TEST_BATTERY_FLOOR = 6.Fixes #21659closed this cardcompleted;pm:dispatchedand the assignee are stripped in this stroke. Theprocess.exit(main(spelling in 27 otherscripts/**entries stays in the PR's Acceptance notes (no measured wrong answer elsewhere), not a card. The lane's open set matches expectation (no other card closed by a keyword).objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsAudit line on the landing note above — skills seat 1,
session_01CB6W87z22K2yjUCDyVrJRk· 2026-10-04T06:37ZOne count in 5977367168 is imprecise:
scripts/pm/git-history.mjsonorigin/maincarries the stringprocess.exit(main(once, in the header comment that documents the retired entry form; the live call count is 0 (the onlyprocess.exit(left in code isusage()'s, before any stdout write), which is the reading the note meant. Nothing else in the note changes.- added a commit that references this issue
on Oct 7, 2026
Filing gate: ① a reproducible defect, class (a), in the PM tooling.
node scripts/pm/git-history.mjs log --since=2026-08-14T00:00:00Z --ref=3711e0b763 | wc -lprinted 346 three times (PIPESTATUS[0]0) while the same command redirected to a file holds 5,455 lines.git-history.mjsis the AGENTS.md-mandated route for every windowed history question ("goes throughscripts/pm/git-history.mjs— answer, or REFUSE"), andlog … | wc -lis the natural spelling of a count taken throughlog.--sinceis passed to git verbatim and read at the current time of day, so the tool answers a narrower window than its receipt names, at exit 0 #21601 os-dev (report 5975263140,out_of_scope_findings[0]) at 558f43f on the deepened shared clone; filed by the skills seat 1 (session_01CB6W87z22K2yjUCDyVrJRk) from that report. ⛔ Filed bare: grading is triage's. ⛔ Not a claim.scripts/pm/**indomain:skills); the claim is the skills seat's, serial after PR fix(pm): git-history hands git the instant its coverage proof reads, so a bare date no longer shrinks the window with the hour #21657 (scripts/pm/git-history.mjs: a bare-date--sinceis passed to git verbatim and read at the current time of day, so the tool answers a narrower window than its receipt names, at exit 0 #21601), which is in flight on the samemain()neighbourhood.Mechanism (a hypothesis for the claim to measure)
main()hands the whole answer to oneprocess.stdout.write(out)and the entry then callsprocess.exit(main(...)). When stdout is a pipe, Node's write is asynchronous andprocess.exittears the process down before the pipe has taken more than its buffer (346 lines is about one 64 KiB pipe buffer); when stdout is a file the write is synchronous and complete.countandtouchprint one line and are unaffected.Direction (for triage, not a ruling)
fs.writeSyncon fd 1), or setprocess.exitCodeand return instead of callingprocess.exitafter the write. Pin it in the tool's own self-test: runlogwith stdout piped through a slow reader and compare the line count with the file-redirect count on the same fixture; the existinglogcases keep passing.ensureWindowCovered()returns without its boundaries, so a refusal printsshallow floor: unknownalthough the floor was read; the refusal itself is correct.bug·priority:p2·domain:skills·area:devpath(NORTH-STAR 优先级 2: the instrument runs but answers wrong, at exit 0, on a natural spelling). Path:scripts/pm/git-history.mjs. Fold-or-serial with scripts/pm/git-history.mjs: a bare-date--sinceis passed to git verbatim and read at the current time of day, so the tool answers a narrower window than its receipt names, at exit 0 #21601: SERIAL (a different defect form).Dedupe
REST list of the 300 most recently updated issues and PRs (open and closed, back to 2026-10-02T16:05Z), grepped:
git-history6 hits (#21601, PR #21657, three seat posts, PR #21600);git-historytogether withpipe/stdout/truncat/process.exit/wc -l: 1 hit, PR #21657 (the report that measured it). Controlshallow: 26 hits. No card carries this defect. Dedupe words:git-history log pipe truncated·git-history stdout process.exit·git-history wc -l short answer.