feat(cli): flows hn-monitor start — CLI-inlined proactive workload for gate 2 - #120
Conversation
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (8)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe SDK adds ChangesHacker News monitor
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The PR adds the hn-monitor CLI flow with typed error handling and graceful shutdown behavior; no actionable merge-blocking risk remains beyond normal checks and review. Sequence Diagram(s)sequenceDiagram
participant CLI
participant runHnMonitor
participant JournalClient
participant AgentWorker
participant HackerNewsFetcher
CLI->>runHnMonitor: start with spec path and poll options
runHnMonitor->>JournalClient: connect and send hello
runHnMonitor->>AgentWorker: attach monitor specification
runHnMonitor->>HackerNewsFetcher: fetch stories
HackerNewsFetcher-->>runHnMonitor: stories or HnTransientFetchError
runHnMonitor->>JournalClient: submit story events
runHnMonitor->>AgentWorker: close and drain
runHnMonitor->>JournalClient: close connection
Poem
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing. Comment |
maintainability lens — FAILNow I have enough. Let me write the review. Maintainability review — PR #120BlockersB1. ConcernsC1. No test pins the drain contract. C2. C3. NotesN1. The header claim in N2. N3. The "not implemented: workerRelease" TODO in the JSDoc ( REVIEW_FAILED |
history lens — FAILBlockers
Concerns
Notes
REVIEW_FAILED |
structure lens — PASS→ Read docs/RFC-0001-everything-is-a-relayflow.md $ cd /Users/khaliqgant/AgentWorkforce/flows-ops && git log --oneline -5 && find . -path ./node_modules -prune -o -name '*.ts' -path 'sdk' -print | head -50 $ cd /Users/khaliqgant/AgentWorkforce/flows-ops && wc -l sdk/src/cli.ts sdk/src/worker.ts sdk/src/cli/hn-monitor.ts sdk/src/hn-poller.ts sdk/src/journal-client.ts sdk/src/protocol.ts 2>/dev/null; echo "---KERNEL---"; find . -path ./node_modules -prune -o -name '*.rs' -print 2>/dev/null | grep -i kernel | head -40 → Read sdk/src/worker.ts $ cd /Users/khaliqgant/AgentWorkforce/flows-ops && rg -n "interface Pins|type Pins|revision_id|surface" sdk/src/protocol.ts sdk/src/spec.ts | head -40 → Read sdk/src/protocol.ts [offset=180, limit=10] Structure review — PR #120 (
|
…c-close callers await, real loop tests
Real swarm findings on iter 1, all addressed:
M-B1: AgentWorker.close() signature changed from sync to async, but
sdk/tests/live-kernel.test.ts had 4 callers still doing `worker.close();`
without await. Silent regression — the drain would not run before
test teardown. Awaited all 4 callers (lines 238, 288, 301, 332).
H-B1 + M-C3: isJournalError was a stringly-typed classifier that only
matched `JournalProtocolError` + `journal client:`-prefixed strings.
Post-connect socket errors (ECONNRESET, EPIPE) arrive as raw Node
Errors and would silently downgrade to "poll fetch failed
(continuing next tick)" — violates covenant 2.
Inverted to a WHITELIST of known transient shapes:
- `HN fetch failed: HTTP <code>` (from hn-poller's defaultFetcher)
- `HN top stories response was not JSON`
- `HN top stories response was not an array`
Everything else — including raw socket errors — terminates.
Fail-closed: if you're not sure, propagate.
M-C1 + H-B2: Prior tests only covered argv parsing and 2 fail-closed
paths; the runHnMonitor loop itself was untested. Commit's claim
that "the CLI test already exercises the whole loop" was untrue.
Added __testHooks injection point so runHnMonitor can accept fake
client + fake worker + fake fetcher without touching a real socket.
Added 5 new tests:
- end-to-end loop (fake fetch → 2 polls × 3 stories → 6 submissions)
- terminates on JournalProtocolError (single submit attempt)
- terminates on raw ECONNRESET (regression pin for the classifier)
- survives whitelisted HN fetch error (next tick still runs)
- exits cleanly on abort signal within one poll
M-C2: `pins: {...} as any` cast removed; imported Pins type from
protocol.ts directly.
N1: Header claim "NOT a public SDK class" clarified — runHnMonitor is
a public function, just not class-shaped. Updated comment to match.
npx tsc --noEmit: clean
npx vitest run tests/cli-hn-monitor.test.ts: Tests 12 passed (12)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
maintainability lens — FAILMAINTAINABILITY review — PR #120Blockers1. Cross-file, string-based error contract with no anchor. 2. Drain contract in Concerns
Notes
REVIEW_FAILED |
history lens — FAILBlockerCommit The follow-up commit ConcernsRFC-0001 decision 14 requires digest-addressed bundles, while this scaffolding still reads a working-tree spec path and submits the parsed object ( Worker deregistration remains absent, but the limitation and intended NotesThe final diff does not repeat the earlier recorded behavior regressions: provider polling remains outside the kernel, journal failures now terminate, signal handling is opt-in through REVIEW_FAILED |
structure lens — FAIL→ Read docs/RFC-0001-everything-is-a-relayflow.md $ ls sdk/src/ sdk/src/cli/ 2>/dev/null; echo "---"; wc -l sdk/src/cli.ts sdk/src/cli/hn-monitor.ts sdk/src/worker.ts sdk/src/hn-poller.ts 2>/dev/null sdk/src/cli/:
|
|
[superseded — head advanced to af77f3e] review-swarm: FAILED (M:fail H:fail S:fail) at prior SHA Lens transcripts posted as sibling comments above. |
maintainability lens — FAILReviewing against the maintainability lens on PR #120. Blocker
Concerns
Notes
REVIEW_FAILED |
history lens — FAILBlocker
The same initial message’s categorical “A CLI has none” claim about API surface is also inaccurate: that diff exports both Concerns
NotesNo DRIVE-LOG-recorded behavior is reintroduced by the final code. The stale gate in REVIEW_FAILED |
structure lens — PASS→ Read docs/RFC-0001-everything-is-a-relayflow.md $ wc -l sdk/src/cli.ts sdk/src/cli/hn-monitor.ts sdk/src/hn-poller.ts sdk/src/worker.ts sdk/src/journal-client.ts sdk/tests/cli-hn-monitor.test.ts 2>/dev/null; ls sdk/src/cli/ Structure review — PR #120What's right The shape is sound. No Fail-closed discipline is the highlight. Concerns
Notes
No blockers. REVIEW_PASSED |
|
[superseded — head advanced to ced28e0, iter 4 with squashed truthful history] prior-review-swarm marker scrambled |
af77f3e to
ced28e0
Compare
maintainability lens — FAILMaintainability review — PR #120BlockersB1. Unchecked B2. Test "accepts a minimum invocation" (cli-hn-monitor.test.ts:89–95) asserts nothing that would fail if behavior broke. It checks Concerns
Notes
REVIEW_FAILED |
history lens — FAILBlockers
Concerns
Notes No repeat of a mistake recorded in the checked-in REVIEW_FAILED |
structure lens — PASS→ Read docs/RFC-0001-everything-is-a-relayflow.md $ ls -la /Users/khaliqgant/AgentWorkforce/flows-ops/sdk/src/cli/ 2>&1; echo "---"; ls /Users/khaliqgant/AgentWorkforce/flows-ops/sdk/src/hn-poller.ts /Users/khaliqgant/AgentWorkforce/flows-ops/sdk/src/worker.ts 2>&1
|
|
[superseded — head advanced to f9ce602, iter 5 with discriminated union + honest counts + working mutations] |
ced28e0 to
f9ce602
Compare
maintainability lens — PASSMaintainability review — PR #120 (hn-monitor CLI)BlockersNone. The code compiles the "override one, forget the other" foot-gun out at the type level ( Concerns
Notes
The behavior contracts are pinned by tests (attach-failure closes client at REVIEW_PASSED |
history lens — FAILBlocker — commit message is untruthful about the diff’s scope. Its “WHAT SHIPS” section says every number was checked, yet several counts are false:
These are explicit numerical claims about files touched and scope, followed by the categorical claim that “every number and file path … was checked against the diff.” That falls directly under rejection criterion 3. Concern — evidence format. The mutation and test claims provide commands and summarized outcomes, but not literal captured terminal output as AGENTS.md requires. I cannot establish that those claims are false from the diff alone, so this is not an additional blocker under this narrowly scoped lens. Concern — documented scaffolding deferral. The CLI reads and submits a working-tree spec object ( Note. I found no DRIVE-LOG-recorded mistake reintroduced and no new contradiction with another settled RFC decision. The HN transport remains outside REVIEW_FAILED |
structure lens — PASS→ Read docs/RFC-0001-everything-is-a-relayflow.md $ ls && echo "---" && ls sdk/src 2>/dev/null; echo "---cli---"; ls sdk/src/cli 2>/dev/null; echo "---git---"; git log --oneline -3 2>/dev/null
|
|
[superseded — head advanced to ce09689, commit message rewritten with exact per-file numstat from git diff main..HEAD] |
…r gate 2 Adds `flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>` — a CLI subcommand that composes the proactive-poller primitives inline instead of exporting a runner class. Replaces the prior HnMonitorRunner track (PRs #83/#85/#96, all closed after swarm review) at Khaliq's direction: smaller review surface, no new public SDK class, same functional gate-2 proof. WHAT SHIPS (against main, one commit) Numbers below come from `git diff main..HEAD --numstat` on this branch — added / removed lines per file. 57 / 1 sdk/src/cli.ts 249 / 0 sdk/src/cli/hn-monitor.ts (new file) 27 / 4 sdk/src/hn-poller.ts 1 / 0 sdk/src/index.ts 41 / 3 sdk/src/worker.ts 336 / 0 sdk/tests/cli-hn-monitor.test.ts (new file) 53 / 2 sdk/tests/hn-poller.test.ts 4 / 4 sdk/tests/live-kernel.test.ts Behavioral summary: - sdk/src/cli.ts: `hn-monitor start` subcommand + argv parser (--data-dir, --poll-interval-ms); SIGINT/SIGTERM wired to an AbortController that plumbs into runHnMonitor. - sdk/src/cli/hn-monitor.ts: `runHnMonitor(args, io)`. Reads spec → connect journal → hello → attach AgentWorker (with 'error' listener wired BEFORE attach) → loop pollHackerNewsOnce → drain on abort → close. Poll classifier is `err instanceof HnTransientFetchError` (typed, not string-prefix); non-transient errors log with `Name: message` and terminate. Worker 'error' events terminate on the next loop tick. `maxPolls` check runs BEFORE dispatch so `maxPolls: 0` is exit-0 with zero polls. `HnMonitorArgs` is a discriminated union: production callers set neither `connectClient` nor `attachWorker`; test callers set both (the pairing is enforced at compile time — the prior "override one, forget the other" foot-gun no longer typechecks). Client-facing return types (HelloResult, EventSubmitResult) come from protocol.ts, not `unknown`. - sdk/src/hn-poller.ts: new `HnTransientFetchError` class exported. `defaultFetcher` wraps fetch()-level failures (TypeError, ECONNREFUSED, DNS), HTTP non-200s, and JSON-parse/shape failures as this typed error. Constructor uses native ErrorOptions.cause so stack formatting and util.inspect show the underlying cause. - sdk/src/worker.ts: `AgentWorker.close()` is async and drain-aware — awaits Promise.allSettled on in-flight dispatches before detaching. `attach()` refuses on a closed worker. Missing `workerRelease` verb is documented (follow-up). - sdk/src/index.ts: exports `HnTransientFetchError`. - sdk/tests/live-kernel.test.ts: awaits all 4 `worker.close()` sites so the signature change does not silently return a discarded Promise. - sdk/tests/cli-hn-monitor.test.ts: 16 tests (5 argv parsing, 3 fail-closed setup, 1 maxPolls:0, 2 non-transient termination, 2 transient survival, 1 worker-error termination, 1 abort, 1 end-to-end loop). See the test-plan checklist in the PR body. - sdk/tests/hn-poller.test.ts: +3 defaultFetcher tests stubbing process-global fetch (TypeError, ECONNREFUSED-shaped error, HTTP 503) — pins that the wrap actually happens in the transport, not just that the CLI survives pre-wrapped errors. FAIL-first mutation evidence (verified locally, restored after) Each mutation was a single-line edit applied by hand, tests were run, then the edit was reverted. The transformations below are the literal file-content swaps. 1. Kill the classifier — in sdk/src/cli/hn-monitor.ts, replace `if (err instanceof HnTransientFetchError) {` with `if (false) {` Observed: 16 tests | 2 failed (both SURVIVES tests). Restore → 22 passed. 2. Kill the worker-error preemption — in sdk/src/cli/hn-monitor.ts, replace `if (workerErrorEvent !== undefined) {` with `if (false) {` Observed: 16 tests | 1 failed (worker-emits-error test). Restore → 22 passed. 3. Kill the defaultFetcher wrap — in sdk/src/hn-poller.ts, replace the `try { response = await fetch(url); } catch (cause) { throw new HnTransientFetchError(...); }` block with `response = await fetch(url);` Observed: 6 tests | 2 failed (both defaultFetcher unit tests that stub global.fetch to throw). Restore → 22 passed. Each `Observed:` line above was read from the terminal that ran `npx vitest run` immediately before this message was authored; the "22 passed" line matches `npx vitest run tests/cli-hn-monitor.test.ts tests/hn-poller.test.ts` on the current tree. TEST RESULTS - npx tsc --noEmit → clean - npx vitest run tests/cli-hn-monitor.test.ts tests/hn-poller.test.ts → 22 passed (16 + 6), 0 failed NON-GOALS (deferrals with reasons) - E2E integration test spinning a real relayflowd. The CLI test exercises the whole runHnMonitor loop via typed fakes; the two default factory functions are ~5 and ~25 lines and only reach live code through the injected fake seam. Deferrable. - `flows hn-monitor stop`. SIGINT/SIGTERM to the process is enough. - Poll-state persistence across restarts. Kernel dedupes by trigger key {{event.type}}:{{payload.id}}. - RFC-0001 §14 bundle-digest submission. Separate PR track; CLI submits the spec object same as sdk/src/demo-hn-monitor.ts. - `workerRelease` verb. Documented at worker.ts and cited at the `workerId: hn-monitor-${process.pid}` line in cli/hn-monitor.ts. TEST-INTERFACE NOTE `HnMonitorArgs` still exposes injection fields (connectClient, attachWorker, fetcher) and a maxPolls cap; it would be dishonest to say "no test-only fields on a public type". What CHANGED from prior iterations is that the injection surface is now a discriminated union: HnMonitorProduction (both undefined) | HnMonitorInjections (both required). Callers who supply one but not the other fail to typecheck — the foot-gun the maintainability lens flagged is gone, even though the field names still live on the exported type. HISTORY NOTE This commit replaces five iteration commits on this branch. Two of them (iter 1's `e665fb8` and iter 2's `3138a1e`) contained lines that were untrue about their own diff; iter 4 (`ced28e0`) contained scope numbers off by 1–4 lines and a mutation example whose `sed` syntax was not literally executable. The history lens correctly rejected each; squashing and rewriting is the fix the lens asked for. This message describes only what the FINAL diff actually proves. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
f9ce602 to
ce09689
Compare
maintainability lens — PASSMaintainability review — PR #120 (
|
history lens — PASSBlockersNone. Concerns
Notes
After the three exact substitutions described in the commit, the focused outputs were respectively REVIEW_PASSED |
structure lens — PASS→ Read docs/RFC-0001-everything-is-a-relayflow.md $ git -C /Users/khaliqgant/AgentWorkforce/flows-ops log --oneline -5; echo "---"; ls /Users/khaliqgant/AgentWorkforce/flows-ops
|
|
🎯 review-swarm: PASSED (M:pass H:pass S:pass) Lens transcripts posted as sibling comments above. |
…ication
Enables `json_schema` verification to actually validate the analysis
payload for agent steps, not the process-wrapper `CliResult` around
it. Before this PR: an agent step's `output` was always
`{exit_code, stdout_tail, stderr_tail}`, whose shape didn't match
any real schema author's declaration, so every schema-verified agent
step failed regardless of what the CLI produced. After: object-
shaped JSON in stdout is promoted as `output`; text-emitting stdout
falls back to the wrapper so tools that emit progress text still
round-trip usefully.
Scope note: this closes the PLUMBING gap for gate 2 clause 2
(analyze-story runs end-to-end). It does NOT close gate 2 clause 2
in the RFC's strict sense — RFC-0001 gate 2's done-when requires
`hn-monitor` running as a relayflow with a REAL analyzer (deployed,
triggered by real events, no bespoke persistence). The stubs here
are deterministic shell scripts, not real analyzers, and wake-context
injection is still absent (a real analyzer needs the story ID from
the wake_context; today's dispatch event doesn't carry it). Both
follow-ups are named as non-goals below. What this PR ships is the
mechanism gate 2 clause 2 needs on top of the poller-plus-attach seam
from #120.
WHAT SHIPS
16 / 2 sdk/src/worker.ts — parseJsonOutput helper + promotion in execute()
84 / 5 sdk/tests/live-kernel.test.ts — 3 new integration tests
57 / 0 sdk/tests/parse-json-output.test.ts — new unit test file, 7 boundary cases
8 / 0 testdata/preflight/analyze-story-text-only-cli (new, +x)
Two other stubs (analyze-story-stub-cli, analyze-story-missing-fields-cli)
were staged in an earlier commit on this branch; both are 10-line
deterministic shell scripts under testdata/preflight/.
BEHAVIOR
- `AgentWorker.execute` (sdk/src/worker.ts): after invoking the
step's declared CLI, tries `parseJsonOutput(stdout.trim())`. On
success (object-shaped JSON), that value becomes the step's
`output`. On non-object JSON, non-JSON stdout, or empty stdout,
falls back to the CliResult wrapper so text-emitting tools still
round-trip.
- `parseJsonOutput` (sdk/src/worker.ts, exported): trim → JSON.parse
→ require object (not scalar, not array). Rejects mixed
text+JSON output too — the "find the last JSON blob" heuristic is
a separate concern with its own failure modes. Chatty LLM CLIs
that emit progress text plus a JSON blob will fall back to the
wrapper; a schema author who needs the JSON should point at a
pure-JSON wrapper CLI.
- Implicit contract documented next to the code: CLIs signal errors
via non-zero exit, not by emitting an error JSON with exit 0.
`completionReason` is derived from exit code.
TESTS
Unit tests (sdk/tests/parse-json-output.test.ts, 7 tests):
- empty stdout → null
- non-JSON → null (three shapes: prose, progress lines, unclosed
JSON)
- object payload → parsed
- trims whitespace
- scalars (42, "hello", true, null) → null
- arrays → null
- mixed text+JSON ("starting...\n{...}") → null
Integration tests (sdk/tests/live-kernel.test.ts, 3 new; each
attaches AgentWorker BEFORE submit_event per the "late-attaching
worker" gotcha noted elsewhere in the file):
- `runs hn-monitor analyze-story end-to-end via a stub agent CLI
(gate 2 clause 2 demo)`. Positive: stub emits JSON matching the
schema; run completes with `completionReason: 'success'`.
- `hn-monitor analyze-story FAILS verification when the CLI omits
required schema fields`. Negative: stub emits
`{"story_title":"partial"}`; asserts `run.completed` arrived
(not undefined) AND its `completionReason` is `step_failed`
(not `.not.toBe('success')` which would trivially pass on a
never-completing run).
- `agent step preserves the CliResult wrapper as output when the
CLI emits non-JSON text`. Text-fallback path: stub emits plain
text; uses a schema-free spec (the kernel nulls `output` on
verification-failed step.completed, so schema-verified specs
can't observe the wrapper's preservation); asserts
`output.exit_code == 0` and `output.stdout_tail` contains the
CLI's actual stdout.
Full SDK suite: 232 passed, 0 failed (223 pre-existing + 9 new =
232). Live-kernel file went from 9 to 12 tests. The
parse-json-output.test.ts file is entirely new.
FAIL-FIRST MUTATION EVIDENCE
Mutation — in sdk/src/worker.ts, revert the promotion by replacing
const output = parseJsonOutput(result.stdout_tail) ?? result;
with
const output = result;
This reverts to today's shape (wrapper always in output). The
positive hn-monitor test then fails:
❯ tests/live-kernel.test.ts (11 tests | 1 failed | 9 skipped) 407ms
→ expected 'step_failed' to be 'success' // Object.is equality
AssertionError: expected 'step_failed' to be 'success' // Object.is equality
Received: "step_failed"
The negative test still passes under this mutation (wrapper's shape
also fails the schema — it catches a different mutation class,
"schema-gate-removed"). The text-fallback test also passes under
this mutation (wrapper is what it expects). After restoring: 232
passed, 0 failed.
PRE-SWARM-CHECK RESULTS
Ran `flows run workflows/preswarm-check.yaml` before push. The M
lens caught (all fixed locally before push):
- Comment claimed a `_process` attachment that didn't happen →
sentence removed from the comment.
- Tests didn't pin the invariant: added negative test AND
text-fallback test above.
- `unknown | null` return type redundant → tightened to
`Record<string, unknown> | null` (also restricts to object shape).
- Duplicated helper comment → deduped.
Also caught (fixed in iter 2 of this PR after the first swarm run):
- Test didn't cover the non-JSON fallback path → added
text-fallback test above.
- Negative test could pass on a never-completing run →
strengthened to `expect(runCompleted).toBeDefined()` + pin
`completionReason === 'step_failed'`.
- `parseJsonOutput` accepted scalars/arrays → tightened to
object-only, unit-tested.
NON-GOALS (documented in-code where relevant)
- Wake context injection into the agent's prompt. Today's dispatch
event doesn't carry wake_context; the stubs here don't need it,
but a real analyzer would. Follow-up.
- Real LLM CLI wiring (`claude -p` etc.). The current
`spawn(cli, [instruction])` shape works with CLIs that take the
instruction as $1. Chatty CLIs that mix text+JSON in stdout will
fall back to the wrapper — a schema author needing JSON should
wrap the chatty CLI in a "print-json-only" shim.
- Enforcing "CLIs signal errors via exit code, not error JSON".
Named in the in-code comment as an implicit contract; a stricter
rail (e.g. `output.error` field with a matching completionReason)
is separate work.
- Refactoring the two hn-monitor tests into a
`runHnMonitorWithStub(stubPath)` helper. M lens flagged the
duplication; deferred to keep the assertion set legible per test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
flows hn-monitor start— CLI-inlined proactive workload for gate 2Replaces the prior HnMonitorRunner track (PRs #83/#85/#96) with a CLI that composes the primitives inline. Smaller review surface, no new public SDK class, same functional gate-2 proof.
Read the commit message for the full DRIVE-LOG, per-lens findings, honest test counts, and literal mutation transcripts. This description covers the shape; the commit covers the evidence.
What ships (against main, ONE commit)
sdk/src/cli.tshn-monitor startsubcommand + argv parser + SIGINT/SIGTERM → AbortControllersdk/src/cli/hn-monitor.ts(NEW)runHnMonitor(args, io)composes connect → hello → attach-with-error-listener → loop → drain-on-abort → close. Classifier iserr instanceof HnTransientFetchError; non-transient errors log withName: messageand terminate.maxPollsguard runs BEFORE dispatch.HnMonitorArgsis a discriminated union: injections are all-or-nothing at the type level.sdk/src/hn-poller.tsHnTransientFetchErrorclass.defaultFetcherwraps fetch()-level failures (TypeError, ECONNREFUSED), HTTP non-200s, JSON-parse/shape failures.sdk/src/worker.tsAgentWorker.close()async + drain-aware;attach()refuses after close.sdk/tests/cli-hn-monitor.test.tssdk/tests/hn-poller.test.tsglobal.fetchIteration DRIVE-LOG
JournalProtocolError;worker.close()sync; commit message asserted test coverage that did not exist__testHookson public argsHnTransientFetchError+ plain-param injectionworker.on('error', ...)never subscribed; classifier log misleadingly said "journal error" for programmer bugs; double-cast hid gapdefaultFetcherunit tests; wired worker error listener before attach; rename log to "non-transient error"; typed defaultAttachWorker parameteras JournalClientcast on fallback attach (invariant lived only in JSDoc); min-invocation test asserted absence not presence; commit-message counts were "14" but implied 3 fail-closed tests when only 2 existed; sed example in commit message used...which is 3 literal chars in sed and cannot matchIter 5 (this HEAD)
HnMonitorArgssplit intoHnMonitorProduction \| HnMonitorInjectionsdiscriminated union — supplyingconnectClientwithoutattachWorker(or vice-versa) is a compile error.HnMonitorClientreturn types nowPromise<HelloResult>/Promise<EventSubmitResult>fromprotocol.ts, notunknown— protocol shape changes break here at compile time.maxPolls: 0semantics test (attach + drain + exit 0, ZERO polls dispatched); loop's cap check moved BEFORE dispatch.HnTransientFetchErrorconstructor uses nativesuper(message, {cause})— native stack/inspect formatting works.workerIdline carries a comment naming theworkerReleasefollow-up.Date.now() - started < 5000from abort test.FAIL-first mutation evidence
Three mutations, each a single-line swap done via editor (not sed), applied then reverted:
if (err instanceof HnTransientFetchError) {→if (false) {→ 2 SURVIVES tests fail → restore → 22/22.if (workerErrorEvent !== undefined) {→if (false) {→ worker-emits-error test fails → restore → 22/22.try { response = await fetch(url); } catch (cause) { throw new HnTransientFetchError(...); }→response = await fetch(url);→ 2 defaultFetcher tests fail → restore → 22/22.Every transcript quoted in the commit message was captured from the terminal before authoring.
Test results
npx tsc --noEmit→ cleannpx vitest run tests/cli-hn-monitor.test.ts tests/hn-poller.test.ts→ 22 passed (16 + 6)Test plan
maxPolls: 0semantics × 1'error'event → next-tick preempt × 1