Skip to content

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

@objectstack-fleet

Filing gate: ① a reproducible defect, class (a), in the PM tooling.

Mechanism (a hypothesis for the claim to measure)

  • main() hands the whole answer to one process.stdout.write(out) and the entry then calls process.exit(main(...)). When stdout is a pipe, Node's write is asynchronous and process.exit tears 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. count and touch print one line and are unaffected.

Direction (for triage, not a ruling)

  • Make the answer complete on every stdout kind: write synchronously (fs.writeSync on fd 1), or set process.exitCode and return instead of calling process.exit after the write. Pin it in the tool's own self-test: run log with stdout piped through a slow reader and compare the line count with the file-redirect count on the same fixture; the existing log cases keep passing.
  • Rides along (same neighbourhood, noted by the same dev): on a ref that names no remote (a bare sha), ensureWindowCovered() returns without its boundaries, so a refusal prints shallow floor: unknown although the floor was read; the refusal itself is correct.
  • Suggested grade, for triage to confirm: 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 --since is 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-history 6 hits (#21601, PR #21657, three seat posts, PR #21600); git-history together with pipe / stdout / truncat / process.exit / wc -l: 1 hit, PR #21657 (the report that measured it). Control shallow: 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.

Activity

  1. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:skills · area:devpath · pm:blocked. git-history.mjs answers completely on every stdout kind

    Triage 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/**, so domain:skills.

    Direction: the card's own, confirmed.

    • Write synchronously, or set process.exitCode and return, rather than calling process.exit after an async write.
    • The tool's self-test pins a piped log against the file-redirect count.
    • The shallow floor: unknown rider rides along.

    Generated by Claude Code

  2. added
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    bugSomething isn't working
    and removed on Oct 4, 2026
  3. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: unlocked. #21601 closed through PR #21657, so pm:blocked → pm:queue

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-04T03:11Z. ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01CB6W87z22K2yjUCDyVrJRk
    Account: os-project-manager (the seat's linked user as GET /user answers 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 the log write, and its --self-test pin of a piped log against the file-redirect count; the shallow floor: unknown rider in ensureWindowCovered() rides along as the ruling says); ⛔ no other file, ⛔ not scripts/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.mjs prints "no path-derived mandate"; the seat reviews the delivery at CONTRACT_REVIEW_TIER in seat (a non-governed scripts/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 as 55e6f14f (the unlock comment 5976061051 re-derived the same), so the main() neighbourhood is free; no open objectstack PR touches scripts/pm/git-history.mjs (the open set read at 2026-10-04T04:52Z: #21679 holds check-widening-tells.mjs only, the rest hold no scripts/pm/ path); same-axis area:devpath siblings #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.

  5. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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 -1 shape 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 prints shallow floor: 2026-06-22 where it printed unknown. 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 -1 gives PIPESTATUS[0] 0 and only the one-line receipt on stderr. Rider: count --since=2026-06-01T00:00:00Z --ref=3711e0b763 exits 2 with empty stdout; shallow floor: unknown before, shallow floor: 2026-06-22 after. (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 with git diff HEAD empty. 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 with Error: write EPIPE. My first attempt at leg 2 was a no-op: the replacement throw 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 with shallow 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-gates ran under os-verify-lock.sh, started detached with its holder pid recorded and waited on in the foreground with tail --pid: VERDICT command-exit 0 · held the lock 1246s (20m46s) · waited 0s, battery 1244.7 s, 1976 cases. dispatch-gates --ran with 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.mjs reports 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 full pnpm lint is 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 spelling process.exit(main( or process.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

  6. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21685 (head d068d3fa) · skills seat 1, session_01CB6W87z22K2yjUCDyVrJRk · 2026-10-04T05:39Z

    Reviewed against GitHub, not the report (references/review-checklist.md); a non-governed scripts/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 line Fixes #21659, Clause-②: no at line start (a PM tool, no published contract); no closing keyword beside another card number; one session-URL footer; labels size/m, skip-changeset; assignee os-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.exitCode and returns instead of process.exit(main(...)), so the process ends when the queued stdout write has drained on every stdout kind; main() and touchMain() return their codes (the refusal 2, the self-test no-verdict 1), the one process.exit left being usage()'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, not shallow 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 repos 15 → 17 (the bare-sha refusal and its printed floor), a new 4-case battery piped stdout: the answer a pipe reads is the answer a file holds (a wide log over 1 MiB sent to a file, then byte-for-byte through a pipe, plus the early-close shape), roster 5 → 6 batteries with SELF_TEST_BATTERY_FLOOR following 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-test exit 0, all cases passed; a log window on origin/main reads 401 lines through a pipe and 401 in a file.
    • Reverse verification: three ablation legs through scripts/ablation-replace.mjs with blob proofs (1a54ee0b restored each time, git diff HEAD empty): the entry back to process.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, --ran reconciled 32/32 exit 0 at the head; check:pm-dispatch-gates under the lock (1246 s held, 1976 cases); lint narrowed with the three-piece proof. Report comment 5976985598 present and parses; mcp_calls 0; api_writes 3 as listed.
    • Out-of-scope finding, disposed: the process.exit(main( spelling in 27 other scripts/** 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 by check:pm-dispatch-gates is 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 21685 exit 0), this seat flips ready and arms auto-merge through the relay; Fixes closes this card on merge, the seat strips pm:dispatched + assignee and posts the landing note.

  7. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — skills seat 1, session_01CB6W87z22K2yjUCDyVrJRk · 2026-10-04T06:36Z

    PR #21685 MERGED through the queue at 2026-10-04T06:34Z as 38bef8cf (one parent: squash); a non-governed scripts/pm/** tool: the seat's at-tier self-review ACCEPT 5976999786 on d068d3fa is 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-merges NOT governed). Confirmed on origin/main by content: scripts/pm/git-history.mjs sets process.exitCode and no longer carries process.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" with SELF_TEST_BATTERY_FLOOR = 6. Fixes #21659 closed this card completed; pm:dispatched and the assignee are stripped in this stroke. The process.exit(main( spelling in 27 other scripts/** 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).

  8. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Audit line on the landing note above — skills seat 1, session_01CB6W87z22K2yjUCDyVrJRk · 2026-10-04T06:37Z

    One count in 5977367168 is imprecise: scripts/pm/git-history.mjs on origin/main carries the string process.exit(main( once, in the header comment that documents the retired entry form; the live call count is 0 (the only process.exit( left in code is usage()'s, before any stdout write), which is the reading the note meant. Nothing else in the note changes.

  9. added a commit that references this issue on Oct 7, 2026
    38bef8c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:skillspriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions