From 978716e64091c2325acd811cefc7286d3695f451 Mon Sep 17 00:00:00 2001 From: warren Date: Sun, 31 May 2026 10:06:56 +0000 Subject: [PATCH] chore(warren): seeds state --- .seeds/issues.jsonl | 12 +++++++++++- .seeds/plans.jsonl | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.seeds/issues.jsonl b/.seeds/issues.jsonl index 4d02cbf..fc7dcf6 100644 --- a/.seeds/issues.jsonl +++ b/.seeds/issues.jsonl @@ -298,7 +298,7 @@ {"id":"mulch-6871","title":"Add --content flag and accept it as a source for convention content","status":"closed","type":"task","priority":2,"plan_step_index":0,"description":"\nStep 1 of plan pl-21a3.\n\nParent seed: mulch-4f80 — Bug: ml record --type convention content parameter is undiscoverable\nPlan template: bug\nPlan approach: Add a `--content ` named flag and make `collectField` honor it as an additional source for the convention `content` field (positional still works, and an explicit `--content` flag should win for clarity). Rewrite the missing-required…\n\nRun `sd plan show pl-21a3` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-27T15:02:05.758Z","updatedAt":"2026-05-27T15:08:29.708Z","plan_id":"pl-21a3","blocks":["mulch-ca83","mulch-4f80"],"closedAt":"2026-05-27T15:08:29.708Z"} {"id":"mulch-2f54","title":"Replace tautological missing-content error with a concrete retry example","status":"closed","type":"task","priority":2,"plan_step_index":1,"description":"\nStep 2 of plan pl-21a3.\n\nParent seed: mulch-4f80 — Bug: ml record --type convention content parameter is undiscoverable\nPlan template: bug\nPlan approach: Add a `--content ` named flag and make `collectField` honor it as an additional source for the convention `content` field (positional still works, and an explicit `--content` flag should win for clarity). Rewrite the missing-required…\n\nRun `sd plan show pl-21a3` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-27T15:02:05.758Z","updatedAt":"2026-05-27T15:13:48.980Z","plan_id":"pl-21a3","blocks":["mulch-ca83","mulch-4f80"],"closedAt":"2026-05-27T15:13:48.980Z"} {"id":"mulch-ca83","title":"Add tests for --content flag and improved error message","status":"closed","type":"task","priority":2,"plan_step_index":2,"description":"\nStep 3 of plan pl-21a3.\n\nParent seed: mulch-4f80 — Bug: ml record --type convention content parameter is undiscoverable\nPlan template: bug\nPlan approach: Add a `--content ` named flag and make `collectField` honor it as an additional source for the convention `content` field (positional still works, and an explicit `--content` flag should win for clarity). Rewrite the missing-required…\n\nRun `sd plan show pl-21a3` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-27T15:02:05.758Z","updatedAt":"2026-05-27T15:21:31.977Z","plan_id":"pl-21a3","blockedBy":["mulch-6871","mulch-2f54"],"blocks":["mulch-da82","mulch-4f80"],"closedAt":"2026-05-27T15:21:31.977Z"} -{"id":"mulch-da82","title":"Release: run /release per .claude/commands/release.md","status":"closed","type":"task","priority":2,"plan_step_index":3,"description":"\nStep 4 of plan pl-21a3.\n\nParent seed: mulch-4f80 — Bug: ml record --type convention content parameter is undiscoverable\nPlan template: bug\nPlan approach: Add a `--content ` named flag and make `collectField` honor it as an additional source for the convention `content` field (positional still works, and an explicit `--content` flag should win for clarity). Rewrite the missing-required…\n\nRun `sd plan show pl-21a3` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-27T15:02:05.758Z","updatedAt":"2026-05-27T15:28:25.134Z","plan_id":"pl-21a3","blockedBy":["mulch-ca83"],"blocks":["mulch-4f80"],"closedAt":"2026-05-27T15:28:25.134Z"} +{"id":"mulch-da82","title":"Release: run /release per .claude/commands/release.md","status":"closed","type":"task","priority":2,"plan_step_index":3,"description":"\nStep 3 of plan pl-21a3.\n\nParent seed: mulch-4f80 — Bug: ml record --type convention content parameter is undiscoverable\nPlan template: bug\nPlan approach: Replace the hard-coded Usage string in `src/cli.ts`'s custom `formatHelp` with Commander's `helper.commandUsage(cmd)` so each subcommand's actual argument signature (including positionals) is rendered. This is a one-line change that fixes…\n\nRun `sd plan show pl-21a3` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-27T15:02:05.758Z","updatedAt":"2026-05-31T10:04:18.678Z","plan_id":"pl-21a3","blockedBy":["mulch-ca83","mulch-3941","mulch-c901"],"blocks":["mulch-4f80"],"closedAt":"2026-05-27T15:28:25.134Z","labels":["bugwatch"]} {"id":"mulch-e7f6","title":"nightwatch patrol: 2026-05-28","status":"open","type":"task","priority":3,"createdAt":"2026-05-28T10:05:40.606Z","updatedAt":"2026-05-28T10:06:37.862Z","labels":["patrol","nightwatch"],"plan_id":"pl-7a81","blockedBy":["mulch-5b9c","mulch-2a17","mulch-1b36","mulch-a8cd","mulch-201a"]} {"id":"mulch-5b9c","title":"Validate --outcome-duration / --duration with strict regex parser","status":"closed","type":"task","priority":2,"plan_step_index":0,"description":"\nStep 1 of plan pl-7a81.\n\nParent seed: mulch-e7f6 — nightwatch patrol: 2026-05-28\nPlan template: refactor\nPlan approach: Targeted, isolated fixes. Each step touches a small set of files and lands as a single PR. Independent steps (parsing, error-coloring, JSON.parse hardening, update.ts JSON output) can ship in any order; the release step is last and blocked…\n\nRun `sd plan show pl-7a81` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-28T10:06:37.862Z","updatedAt":"2026-05-28T10:15:27.857Z","plan_id":"pl-7a81","blocks":["mulch-e7f6"],"closedAt":"2026-05-28T10:15:27.857Z"} {"id":"mulch-2a17","title":"Normalize fatal-error coloring across commands","status":"closed","type":"task","priority":2,"plan_step_index":1,"description":"\nStep 2 of plan pl-7a81.\n\nParent seed: mulch-e7f6 — nightwatch patrol: 2026-05-28\nPlan template: refactor\nPlan approach: Targeted, isolated fixes. Each step touches a small set of files and lands as a single PR. Independent steps (parsing, error-coloring, JSON.parse hardening, update.ts JSON output) can ship in any order; the release step is last and blocked…\n\nRun `sd plan show pl-7a81` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-28T10:06:37.862Z","updatedAt":"2026-05-28T10:24:27.279Z","plan_id":"pl-7a81","blocks":["mulch-e7f6"],"closedAt":"2026-05-28T10:24:27.279Z"} @@ -327,3 +327,13 @@ {"id":"mulch-6145","title":"T4: trim redundant git init/commits from beforeEach","status":"open","type":"task","priority":2,"createdAt":"2026-05-28T19:15:24.041Z","updatedAt":"2026-05-28T19:16:03.243Z","description":"GOAL: Stop paying for real `git init` + git config + commits in beforeEach for tests that do not actually assert git behavior. Real git subprocesses (3-5 per test) dominate several files.\n\nWorking dir: cd /Users/jayminwest/Projects/os-eco/mulch\nINDEPENDENT: no dependency on other Ts.\n\nHOTSPOTS (run real execFileSync('git', ...) in setup):\n- test/commands/sync.test.ts (9.15s/19 tests; initGitRepo + gitCommitAll per test)\n- test/commands/config.test.ts (worktree blocks)\n- test/commands/record.test.ts, test/commands/prime.test.ts\n- test/utils/active-work.test.ts, test/utils/worktree.test.ts\n\nMETHOD:\n- Introduce a small helper (e.g. test/helpers/git.ts withGitRepo(dir)) using `git init -q`, single `git config` for user.name/email, and only commit when the test needs commits.\n- Move git setup OUT of blanket beforeEach and INTO the specific describe/it blocks that assert git-dependent behavior. Tests that only touch .mulch/ files should not init a repo at all.\n- Where a git repo is genuinely required for every test in a file (e.g. sync), keep it but minimize subprocess count (skip extra commits/config not asserted).\n\nVERIFICATION (must exit 0; test count unchanged):\n bun run lint && bun run typecheck\n bun test test/commands/sync.test.ts test/commands/config.test.ts test/commands/record.test.ts test/commands/prime.test.ts test/utils/active-work.test.ts test/utils/worktree.test.ts\n bun run test:ci && bun run report:timing # record reductions\n\nACCEPTANCE:\n- No test loses coverage of git-dependent behavior.\n- Per-test git subprocess count reduced; sync/record/prime/active-work file times drop.\n\nPITFALLS:\n- getMulchDir()/worktree resolution depends on a real .git — keep git for tests that exercise worktree/sync semantics.\n- Do not share a single mutable repo across tests that commit; isolation matters. Per-test temp repos stay, just leaner.","blocks":["mulch-bee4"]} {"id":"mulch-f7bf","title":"T5: adaptive parallel shard runner (core-count agnostic)","status":"open","type":"task","priority":1,"createdAt":"2026-05-28T19:15:54.049Z","updatedAt":"2026-05-28T19:16:03.298Z","description":"GOAL: `bun test` runs all files sequentially in ONE process, so wall-clock = sum of file times. Add a runner that splits test files into N balanced shards and runs them as concurrent `bun test ` processes. Shard count MUST be derived from the runtime environment — never hardcoded to any developer machine spec.\n\nWorking dir: cd /Users/jayminwest/Projects/os-eco/mulch\nINDEPENDENT build (blocks T6).\n\nFILES TO ADD:\n- ADD scripts/test-parallel.ts (run via Bun):\n - Determine shard count: `MULCH_TEST_SHARDS` env > `--shards N` arg > Math.max(1, os.availableParallelism?.() ?? os.cpus().length). Cap to number of test files.\n - Discover test files (glob test/**/*.test.ts).\n - Weight files by historical timing parsed from junit.xml when present (reuse parseJUnit from scripts/report-test-timing.ts), else by file size; greedily bin-pack into shards for balance.\n - Spawn one `bun test ` per shard concurrently (Bun.spawn), stream/prefix output, await all, exit non-zero if ANY shard fails.\n - SEQUENTIAL FALLBACK: if resolved shards <= 1, exec a single plain `bun test` (identical behavior to today).\n- ADD package.json script: \"test:parallel\": \"bun run scripts/test-parallel.ts\".\n- ADD scripts/test-parallel.test.ts: unit-test the pure pieces (shard-count resolution precedence, bin-packing balance, sequential fallback) — pure functions, no real spawning.\n\nDESIGN CONSTRAINTS:\n- Core-count agnostic + cloud-safe: works on a 1-vCPU box (falls back to sequential), a 2-4 vCPU cloud dev box, and large CI runners with the SAME code. No machine-specific constants.\n- Tests already isolate via mkdtemp unique prefixes, so cross-shard temp-dir collisions are not a concern.\n- Deterministic file->shard assignment given the same inputs (stable sort) for reproducibility.\n\nVERIFICATION (must exit 0):\n bun run lint && bun run typecheck\n bun test scripts/test-parallel.test.ts\n MULCH_TEST_SHARDS=1 bun run test:parallel # sequential fallback, full suite green\n MULCH_TEST_SHARDS=4 bun run test:parallel # parallel, same pass/fail outcome + lower wall-clock\n\nACCEPTANCE:\n- Same overall pass/fail result as `bun test` at any shard count.\n- Wall-clock scales down with shard count; sequential fallback exactly mirrors plain `bun test`.\n\nPITFALLS:\n- Aggregate exit code: fail if ANY shard fails (do not mask with tail/pipe).\n- os.availableParallelism may be undefined on older runtimes — guard with cpus().length.\n- Coverage merge across shards is handled in T6, but design spawn args so per-shard `--coverage --coverage-reporter=lcov` with distinct outfiles is possible.","blocks":["mulch-d61d","mulch-bee4"]} {"id":"mulch-d61d","title":"T6: wire CI to sharded runner + coverage merge","status":"open","type":"task","priority":2,"createdAt":"2026-05-28T19:15:54.152Z","updatedAt":"2026-05-28T19:16:03.352Z","description":"GOAL: Use the T5 shard runner in CI with an EXPLICIT, deterministic shard matrix (not derived from runner core count, so behavior is reproducible across runner sizes), merge per-shard coverage, and keep the timing report.\n\nWorking dir: cd /Users/jayminwest/Projects/os-eco/mulch\nDEPENDS ON: T5 (runner must exist).\n\nFILES TO EDIT:\n- .github/workflows/ci.yml:\n - Replace the single test:ci step with a small matrix (e.g. shard: [1,2,3,4], total 4) OR a single job invoking `MULCH_TEST_SHARDS=4 bun run test:parallel` — choose whichever keeps coverage merge simplest; document the choice in the job.\n - Each shard runs `bun test --coverage --coverage-reporter=lcov --coverage-reporter=...` writing a shard-unique lcov path, plus --reporter=junit to a shard-unique junit file.\n - Add a merge step: combine shard lcov files into coverage/lcov.info (concatenate/merge) and shard junit files into junit.xml so existing check:coverage + report:timing + report:quality keep working unchanged.\n - Keep uploading coverage/lcov.info and junit.xml artifacts (VAL-CROSS-003) and appending report:timing to GITHUB_STEP_SUMMARY.\n\nVERIFICATION (must exit 0):\n bun run lint && bun run typecheck\n # Locally emulate: run the runner with coverage, merge, then:\n bun run check:coverage\n bun run report:timing\n ls -la coverage/lcov.info junit.xml\n\nACCEPTANCE:\n- CI wall-clock for tests drops vs the previous single sequential run.\n- Merged coverage feeds check:coverage with no budget regression; merged junit feeds report:timing.\n- Matrix is explicit/deterministic — independent of the physical runner's core count.\n\nPITFALLS:\n- lcov merge: same source files appear in multiple shards' reports only if executed there; merge must union records, not double-count lines (use a real lcov merge approach, not naive concat if it corrupts totals — verify check:coverage numbers are sane).\n- junit merge must keep total tests = sum across shards for report:timing accuracy.\n- Do not regress check:all ordering or artifact-upload steps.","blockedBy":["mulch-f7bf"],"blocks":["mulch-bee4"]} +{"id":"mulch-3941","title":"Render real Commander usage in custom formatHelp","status":"open","type":"task","priority":2,"plan_step_index":0,"description":"\nStep 1 of plan pl-21a3.\n\nParent seed: mulch-4f80 — Bug: ml record --type convention content parameter is undiscoverable\nPlan template: bug\nPlan approach: Replace the hard-coded Usage string in `src/cli.ts`'s custom `formatHelp` with Commander's `helper.commandUsage(cmd)` so each subcommand's actual argument signature (including positionals) is rendered. This is a one-line change that fixes…\n\nRun `sd plan show pl-21a3` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-31T10:04:18.678Z","updatedAt":"2026-05-31T10:04:18.678Z","plan_id":"pl-21a3","labels":["bugwatch"],"blocks":["mulch-4f80","mulch-c901","mulch-da82"]} +{"id":"mulch-c901","title":"Add regression test for record --help showing positional args","status":"open","type":"task","priority":2,"plan_step_index":1,"description":"\nStep 2 of plan pl-21a3.\n\nParent seed: mulch-4f80 — Bug: ml record --type convention content parameter is undiscoverable\nPlan template: bug\nPlan approach: Replace the hard-coded Usage string in `src/cli.ts`'s custom `formatHelp` with Commander's `helper.commandUsage(cmd)` so each subcommand's actual argument signature (including positionals) is rendered. This is a one-line change that fixes…\n\nRun `sd plan show pl-21a3` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-31T10:04:18.678Z","updatedAt":"2026-05-31T10:04:18.678Z","plan_id":"pl-21a3","labels":["bugwatch"],"blocks":["mulch-4f80","mulch-da82"],"blockedBy":["mulch-3941"]} +{"id":"mulch-7a8e","title":"nightwatch patrol: 2026-05-31","status":"open","type":"task","priority":3,"createdAt":"2026-05-31T10:05:32.135Z","updatedAt":"2026-05-31T10:06:47.487Z","labels":["patrol","nightwatch"],"plan_id":"pl-d2c9","blockedBy":["mulch-7549","mulch-a517","mulch-6785","mulch-e33d","mulch-c6de","mulch-7612","mulch-6a3a"]} +{"id":"mulch-7549","title":"Index custom-type record fields in BM25 search","status":"open","type":"task","priority":2,"plan_step_index":0,"description":"\nStep 1 of plan pl-d2c9.\n\nParent seed: mulch-7a8e — nightwatch patrol: 2026-05-31\nPlan template: refactor\nPlan approach: Land each finding as its own small PR. Group: (1) independent bug/inconsistency fixes that touch disjoint files run in parallel; (2) the Release step gates the whole batch and runs last. No public-API signatures change, no dependencies…\n\nRun `sd plan show pl-d2c9` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-31T10:06:47.487Z","updatedAt":"2026-05-31T10:06:47.487Z","labels":["nightwatch"],"plan_id":"pl-d2c9","blocks":["mulch-6a3a","mulch-7a8e"]} +{"id":"mulch-a517","title":"Fix stale-lock race in withFileLock recovery path","status":"open","type":"task","priority":2,"plan_step_index":1,"description":"\nStep 2 of plan pl-d2c9.\n\nParent seed: mulch-7a8e — nightwatch patrol: 2026-05-31\nPlan template: refactor\nPlan approach: Land each finding as its own small PR. Group: (1) independent bug/inconsistency fixes that touch disjoint files run in parallel; (2) the Release step gates the whole batch and runs last. No public-API signatures change, no dependencies…\n\nRun `sd plan show pl-d2c9` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-31T10:06:47.487Z","updatedAt":"2026-05-31T10:06:47.487Z","labels":["nightwatch"],"plan_id":"pl-d2c9","blocks":["mulch-6a3a","mulch-7a8e"]} +{"id":"mulch-6785","title":"Make ml config honor --json for errors","status":"open","type":"task","priority":3,"plan_step_index":2,"description":"\nStep 3 of plan pl-d2c9.\n\nParent seed: mulch-7a8e — nightwatch patrol: 2026-05-31\nPlan template: refactor\nPlan approach: Land each finding as its own small PR. Group: (1) independent bug/inconsistency fixes that touch disjoint files run in parallel; (2) the Release step gates the whole batch and runs last. No public-API signatures change, no dependencies…\n\nRun `sd plan show pl-d2c9` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-31T10:06:47.487Z","updatedAt":"2026-05-31T10:06:47.487Z","labels":["nightwatch"],"plan_id":"pl-d2c9","blocks":["mulch-6a3a","mulch-7a8e"]} +{"id":"mulch-e33d","title":"Normalize --version --json envelope to match outputJson","status":"open","type":"task","priority":3,"plan_step_index":3,"description":"\nStep 4 of plan pl-d2c9.\n\nParent seed: mulch-7a8e — nightwatch patrol: 2026-05-31\nPlan template: refactor\nPlan approach: Land each finding as its own small PR. Group: (1) independent bug/inconsistency fixes that touch disjoint files run in parallel; (2) the Release step gates the whole batch and runs last. No public-API signatures change, no dependencies…\n\nRun `sd plan show pl-d2c9` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-31T10:06:47.487Z","updatedAt":"2026-05-31T10:06:47.487Z","labels":["nightwatch"],"plan_id":"pl-d2c9","blocks":["mulch-6a3a","mulch-7a8e"]} +{"id":"mulch-c6de","title":"Tighten truncate() sentence-boundary short-circuit","status":"open","type":"task","priority":4,"plan_step_index":4,"description":"\nStep 5 of plan pl-d2c9.\n\nParent seed: mulch-7a8e — nightwatch patrol: 2026-05-31\nPlan template: refactor\nPlan approach: Land each finding as its own small PR. Group: (1) independent bug/inconsistency fixes that touch disjoint files run in parallel; (2) the Release step gates the whole batch and runs last. No public-API signatures change, no dependencies…\n\nRun `sd plan show pl-d2c9` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-31T10:06:47.487Z","updatedAt":"2026-05-31T10:06:47.487Z","labels":["nightwatch"],"plan_id":"pl-d2c9","blocks":["mulch-6a3a","mulch-7a8e"]} +{"id":"mulch-7612","title":"Remove the unused src/utils/index.ts barrel","status":"open","type":"task","priority":4,"plan_step_index":5,"description":"\nStep 6 of plan pl-d2c9.\n\nParent seed: mulch-7a8e — nightwatch patrol: 2026-05-31\nPlan template: refactor\nPlan approach: Land each finding as its own small PR. Group: (1) independent bug/inconsistency fixes that touch disjoint files run in parallel; (2) the Release step gates the whole batch and runs last. No public-API signatures change, no dependencies…\n\nRun `sd plan show pl-d2c9` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-31T10:06:47.487Z","updatedAt":"2026-05-31T10:06:47.487Z","labels":["nightwatch"],"plan_id":"pl-d2c9","blocks":["mulch-6a3a","mulch-7a8e"]} +{"id":"mulch-6a3a","title":"Release: run /release per .claude/commands/release.md","status":"open","type":"task","priority":3,"plan_step_index":6,"description":"\nStep 7 of plan pl-d2c9.\n\nParent seed: mulch-7a8e — nightwatch patrol: 2026-05-31\nPlan template: refactor\nPlan approach: Land each finding as its own small PR. Group: (1) independent bug/inconsistency fixes that touch disjoint files run in parallel; (2) the Release step gates the whole batch and runs last. No public-API signatures change, no dependencies…\n\nRun `sd plan show pl-d2c9` for the full plan (context, alternatives, sibling steps, acceptance criteria).\n","createdAt":"2026-05-31T10:06:47.487Z","updatedAt":"2026-05-31T10:06:47.487Z","labels":["nightwatch"],"plan_id":"pl-d2c9","blockedBy":["mulch-7549","mulch-a517","mulch-6785","mulch-e33d","mulch-c6de","mulch-7612"],"blocks":["mulch-7a8e"]} diff --git a/.seeds/plans.jsonl b/.seeds/plans.jsonl index 257c676..aebed2d 100644 --- a/.seeds/plans.jsonl +++ b/.seeds/plans.jsonl @@ -9,3 +9,4 @@ {"id":"pl-7a81","seed":"mulch-e7f6","template":"refactor","status":"approved","revision":1,"sections":{"context":"Nightwatch patrol on 2026-05-28 found four small quality issues: (1) outcome.duration is parsed with bare Number.parseFloat in three CLI commands, so junk input silently writes NaN to .mulch/expertise/*.jsonl — violating the project's strict-numeric-flag-parsing convention (mx-5b9578, already followed by rank.ts); (2) fatal-error console.error sites have drifted — some use chalk.red('Error: ...'), but the common 'No .mulch/ directory found.' message is plain text in delete/ready/archive/query/prime/edit/restore/search, plus a few generic catch-block 'Error: ${err.message}' lines that are also plain — readability across call sites suffers; (3) the Claude setup recipe parses settings.json with bare JSON.parse and no path context, so a corrupted file throws an unhelpful 'Unexpected token …' error inconsistent with the file:line context convention used by readExpertiseFile (mx-7c199c); (4) the deprecated 'mulch update' command prints a non-JSON warning to stderr even when --json is set, breaking the JSON-output contract every other command honors. Quality gates (bun test, bun run lint, bun run typecheck) are green at HEAD — these are not regressions, just drift that has accumulated.","behavior_invariant":"All existing tests continue to pass. Public CLI surface, command names, flags, and JSON schema shapes are unchanged. Successful command paths produce byte-identical stdout/stderr to before for every input that was valid before. The only observable changes are: (a) invalid --outcome-duration / --duration inputs now exit non-zero with a clear error instead of silently writing NaN; (b) error lines that were plain text become chalk.red-wrapped to match peer commands (no message wording changes); (c) corrupted Claude settings.json yields an error mentioning the file path instead of raw JSON parser output; (d) 'mulch update --json' emits the standard JSON error envelope. No new commands, no new flags, no dependency changes, no public API signature changes.","approach":"Targeted, isolated fixes. Each step touches a small set of files and lands as a single PR. Independent steps (parsing, error-coloring, JSON.parse hardening, update.ts JSON output) can ship in any order; the release step is last and blocked by all four.","steps":[{"title":"Validate --outcome-duration / --duration with strict regex parser","description":"Files: src/commands/record.ts (line ~806), src/commands/edit.ts (line ~128), src/commands/outcome.ts (line ~124). All three currently call `Number.parseFloat(options.outcomeDuration as string)` (or options.duration) with no validation, so `--outcome-duration foo` writes `\"duration\": null` or NaN into the JSONL Outcome object. This violates the strict-numeric-flag-parsing convention (mulch record mx-5b9578) which rank.ts already follows. Fix: introduce a shared helper (e.g. parseStrictNonNegativeNumber in src/utils/format-helpers.ts or a new src/utils/numeric-flags.ts) mirroring the regex+Number() pair in src/commands/rank.ts:31-37, and call it from all three sites. On parse failure, print `Error: --outcome-duration must be a non-negative number (got \"\")` (chalk.red in non-JSON, outputJsonError in JSON mode), set process.exitCode = 1, and return without writing the record. Add unit tests under test/commands/record.test.ts (and edit/outcome equivalents) that pass a bogus value and assert exit code 1 + no JSONL mutation. Do NOT change the flag name or add new flags.","labels":["nightwatch"],"blocks":[]},{"title":"Normalize fatal-error coloring across commands","description":"Plain-text `console.error(\"Error: ...\")` and `console.error(\\`Error: ${...}\\`)` calls have drifted from the chalk.red(`Error: ...`) idiom used by most commands. Specifically: src/commands/delete.ts:303,309; src/commands/ready.ts:177,183; src/commands/archive.ts:201,207; src/commands/query.ts:91,203,209; src/commands/prime.ts:150,162,183,255,283,514,675,681; src/commands/onboard.ts:369; src/commands/edit.ts:202,208; src/commands/restore.ts:154,160; src/commands/search.ts:316,322; src/commands/learn.ts:174; src/commands/move.ts:314,317; src/commands/rank.ts:166,173. Wrap each with chalk.red so non-JSON output is visually consistent with peer commands (delete.ts:60, archive.ts uses it earlier, etc.). Do not change message wording or add new error paths. Verify by running `bun test` and a quick `grep -rn 'console.error(\"Error:\\|console.error(\\`Error:' src/` — should return no matches.","labels":["nightwatch"],"blocks":[]},{"title":"Add file-path context to Claude settings.json parse errors in setup.ts","description":"src/commands/setup.ts at lines 199, 234, and 266 call `JSON.parse(raw) as ClaudeSettings` on the contents of `~/.claude/settings.json` (resolved via claudeSettingsPath(cwd)) with no try/catch, so a corrupted/edited settings file throws a bare `SyntaxError: Unexpected token ...` with no path or hint. The project's convention is to wrap JSONL parse errors with file:line context (mulch record mx-7c199c, see src/utils/expertise.ts:67-76). Fix: extract a small helper inside setup.ts (e.g. `parseClaudeSettings(path: string, raw: string): ClaudeSettings`) that wraps JSON.parse in try/catch and rethrows `Error(`Malformed Claude settings at ${path}: ${err.message}. Edit or remove the file to recover.`)`. Use it from install(), check(), and remove(). No behavior change on the happy path. Add a test in test/commands/setup.test.ts that writes a corrupt settings.json, invokes the claude recipe's check(), and asserts the error message mentions the file path.","labels":["nightwatch"],"blocks":[]},{"title":"Honor --json in the deprecated 'mulch update' command","description":"src/commands/update.ts unconditionally calls `printWarning(...)` and exits with code 1, even when the global --json flag is set. Every other command branches on `program.opts().json === true` and emits a structured envelope via outputJson/outputJsonError. Fix: in registerUpdateCommand's action, capture `const jsonMode = program.opts().json === true;` and call `outputJsonError(\"update\", \"'mulch update' is deprecated. Use 'mulch upgrade' instead.\")` when jsonMode, otherwise the existing printWarning. Keep `process.exitCode = 1` either way. Add a one-line test verifying the JSON envelope shape under --json. No new flags, no rename, the command stays hidden.","labels":["nightwatch"],"blocks":[]},{"title":"Release: run /release per .claude/commands/release.md","description":"Final patrol step. After steps 1-4 land, run /release as documented in .claude/commands/release.md to cut and publish the patch release containing these fixes. Bumps version in package.json and src/cli.ts in lockstep (mulch record mx-5ab2bc), updates CHANGELOG.md (mx-900a53), and runs the usual release ritual.","labels":["nightwatch"],"blocks":[]}],"acceptance":["bun test passes (all 1460+ existing tests plus the new tests added in steps 1, 3, and 4).","bun run lint and bun run typecheck both exit 0.","`grep -rn 'console.error(\"Error:\\|console.error(\\`Error:' src/` returns no matches after step 2.","Running `ml record cli --type pattern --name x --description y --outcome-status success --outcome-duration foo` exits non-zero with a 'must be a non-negative number' error and does NOT append a record to .mulch/expertise/cli.jsonl.","Writing invalid JSON to ~/.claude/settings.json and running `ml setup claude --check` produces an error message that contains the settings.json path, not a bare 'Unexpected token'.","`ml update --json` writes a JSON object with {\"success\": false, \"command\": \"update\", \"error\": ...} to stderr and exits non-zero.","No new CLI commands, flags, or dependencies are introduced. Public API and JSON schema shapes are unchanged."]},"children":["mulch-5b9c","mulch-2a17","mulch-1b36","mulch-a8cd","mulch-201a"],"createdAt":"2026-05-28T10:06:37.862Z","updatedAt":"2026-05-28T10:06:37.862Z","name":"nightwatch patrol fixes 2026-05-28"} {"id":"pl-4fb9","seed":"mulch-3204","template":"feature","status":"done","revision":1,"sections":{"context":"Mulch is the structured-expertise CLI tool in the os-eco ecosystem. The os-eco L5 uplift mission targets Level 5 (≥80% pass rate, matching warren's ceiling per docs/l5-uplift/library/warren-ceiling.md) for every sub-repo by porting templates/l5-toolkit/ (the warren-derived toolkit, finalized 2026-05-27) and authoring repo-specific artifacts. Mulch has NOT started uplift yet — this plan ports the toolkit from scratch and lifts mulch to ~50/63 ≈ ~80% → Level 5 ✓ per the per-repo expected math in docs/l5-uplift/library/warren-ceiling.md. The 6 children of this plan are self-contained: an agent reading `sd show ` has the full target dir, files-to-add/edit, source-of-truth refs, verification commands, rubric criteria closed, commit guidance, and mulch-specific pitfalls without needing the parent mission context.","approach":"Port the os-eco L5 toolkit (templates/l5-toolkit/) into mulch/ in six focused commits, in dependency order. Each child seed targets one feature from the mission's seed-plan-source/mulch.json. Mulch hasn't started, so the first child (mulch-repo-scaffolding) lays down all the baseline configs + devcontainer + env + gitignore + biome extension; subsequent children layer ratchet scripts + budgets, coverage + reporters, AGENTS.md + validator + RUNBOOK + skill, pino logger + governance hooks (pre-commit + dependabot cooldown + labels.yml), and finally the check:all aggregator + CI workflow extension. Each child commits one feature to mulch main; do NOT push (user pushes manually). After the final child seals, the user runs /readiness-report from mulch/ to confirm Level 5 ≥80%.","steps":[{"title":"mulch-repo-scaffolding: port baseline configs + devcontainer + env + gitignore + biome/knip/jscpd/bunfig"},{"title":"mulch-ratchet-scripts: port check-file-sizes + check-debt-markers + budgets"},{"title":"mulch-test-coverage-and-reporters: wire coverage + test:ci + report-test-timing + report-quality-metrics"},{"title":"mulch-agents-md-validator-runbook-skills: author AGENTS.md + validator + RUNBOOK + mulch-expertise skill"},{"title":"mulch-pino-logger-and-governance: add pino + redact + pre-commit hook + dependabot cooldown + labels.yml"},{"title":"mulch-check-all-and-ci-finalize: wire check:all aggregator + extend ci.yml + final verification"}],"acceptance":["VAL-MULCH-002 (naming_consistency), VAL-MULCH-003 (cyclomatic_complexity), VAL-MULCH-005 (duplicate_code_detection), VAL-MULCH-007 (unused_dependencies_detection), VAL-MULCH-012 (devcontainer), VAL-MULCH-017 (env_template), VAL-MULCH-018 (gitignore_comprehensive) closed by step 1","VAL-MULCH-004 (large_file_detection), VAL-MULCH-006 (tech_debt_tracking) closed by step 2","VAL-MULCH-008 (test_performance_tracking), VAL-MULCH-009 (test_coverage_thresholds), VAL-MULCH-015 (code_quality_metrics) closed by step 3","VAL-MULCH-010 (agents_md), VAL-MULCH-011 (agents_md_validation), VAL-MULCH-016 (runbooks_documented), VAL-MULCH-020 (skills) closed by step 4","VAL-MULCH-001 (pre_commit_hooks), VAL-MULCH-013 (structured_logging), VAL-MULCH-014 (log_scrubbing), VAL-MULCH-019 (min_release_age), VAL-MULCH-021 (issue_labeling_system) closed by step 5","VAL-MULCH-FINAL: post-mission /readiness-report from mulch/ shows Level 5 / 5 with pass rate ≥80%; each VAL-MULCH-001..021 criterion at 1/1","From cd /Users/jayminwest/Projects/os-eco/mulch: bun install && bun run lint && bun run typecheck && bun test && bun run check:all all exit 0 after step 6"]},"children":["mulch-409a","mulch-3fda","mulch-8cd7","mulch-d919","mulch-3fd5","mulch-6048"],"createdAt":"2026-05-28T03:22:52.773Z","updatedAt":"2026-05-28T18:57:32.181Z","name":"L5 uplift: mulch"} {"id":"pl-099d","seed":"mulch-5acb","template":"feature","status":"done","revision":1,"sections":{"context":"Mulch is one of five sub-repos targeted by the os-eco L5 readiness uplift mission. Prior /readiness-report audits placed mulch below Level 5 because it lacks the ratchet scripts, governance artifacts, validators, structured logger, and CI wiring that warren (the exemplar at Level 5) ships. The orchestrator has already finalised templates/l5-toolkit/ at the os-eco root; this work ports that toolkit into mulch so the next /readiness-report audit can certify Level 5 (≥80% pass rate) and close every VAL-MULCH-* assertion in docs/l5-uplift/validation-contract.md. Mulch's test directory layout (test/ rather than src/) and existing debug/output channels make this slightly different from the canopy and plot ports already shipped — those deltas are documented in docs/l5-uplift/library/porting-playbook.md.","approach":"Port the L5 toolkit (templates/l5-toolkit/) into mulch in six dependent stages, matching warren's ceiling and closing the VAL-MULCH-* assertions enumerated in docs/l5-uplift/validation-contract.md. Each stage lands in one commit on mulch's working branch (no push) and is gated by bun install / lint / typecheck / test exit 0. Special handling for mulch's test/ directory layout (no bunfig change needed for discovery) and the requirement to route the existing debug/output channels through pino in stage 5 instead of adding a parallel logger. Final stage wires check:all + extends ci.yml and asks the user to run /readiness-report.","steps":[{"title":"mulch-repo-scaffolding"},{"title":"mulch-ratchet-scripts"},{"title":"mulch-test-coverage-and-reporters"},{"title":"mulch-agents-md-validator-runbook-skills"},{"title":"mulch-pino-logger-and-governance"},{"title":"mulch-check-all-and-ci-finalize"}],"acceptance":["All six child seeds exist as dependent open seeds linked to parent mulch-5acb.","Each child seed has a self-contained description (>3000 chars) covering goal, working dir, references to templates/l5-toolkit/ + docs/l5-uplift/library/, files to add/edit, exact verification commands, acceptance rubric criteria, commit guidance, and pitfalls.","After all six children complete, bun install && bun run lint && bun run typecheck && bun test exit 0 from /Users/jayminwest/Projects/os-eco/mulch.","bun run check:all exits 0 after the final child lands.","The agent-readiness auditor (/readiness-report) confirms mulch reaches Level 5 (≥80% pass rate) closing each VAL-MULCH-001..021 plus VAL-MULCH-FINAL."]},"children":["mulch-3744","mulch-5c6f","mulch-f8d1","mulch-cbf4","mulch-9258","mulch-a11b"],"createdAt":"2026-05-28T13:14:50.739Z","updatedAt":"2026-05-28T17:38:48.524Z","name":"L5 uplift: mulch"} +{"id":"pl-d2c9","seed":"mulch-7a8e","template":"refactor","status":"approved","revision":1,"sections":{"context":"Routine nightwatch quality patrol scanned src/, test/, scripts/. Gates are green (bun test 1494 pass, bun run lint, bun run typecheck all clean). Six concrete quality issues surfaced — one search-correctness bug for custom-type records, one concurrency race in the lock recovery path, two CLI output-shape inconsistencies (--version --json and ml config error envelopes), a doubly-aggressive truncate helper, and a dead/unused barrel file flagged by knip. None of these add features; all are localized fixes that preserve public CLI contracts.","behavior_invariant":"Public CLI command names, flags, exit codes, and the {success, command, ...} JSON envelope shape stay unchanged. ml record / ml prime / ml search / ml sync continue to read & write the same JSONL layouts. After fixes: (a) BM25 search returns matches for custom-type records on their type-specific fields (e.g. release_decision.title), not just on tags; (b) withFileLock never unlinks a lock file it did not just observe as stale; (c) every command honors --json by emitting outputJsonError on failure; (d) the --version --json output uses the same indented {success, command, ...} envelope as every other command; (e) truncate(text, maxLen) only short-circuits on a sentence boundary when the boundary is close to maxLen, not when it's at offset 5 with maxLen 200; (f) src/ has no unused barrel files. All 1494 existing tests must still pass with no test deletions.","approach":"Land each finding as its own small PR. Group: (1) independent bug/inconsistency fixes that touch disjoint files run in parallel; (2) the Release step gates the whole batch and runs last. No public-API signatures change, no dependencies added, no new commands. Each step adds or extends unit tests against the existing test suite layout (test/utils/, test/commands/) and tightens the relevant assertion. Use scripts/check-* gates as-is; do not loosen budgets.","steps":[{"title":"Index custom-type record fields in BM25 search","type":"task","priority":2,"labels":["nightwatch"],"blocks":[7],"description":"src/utils/bm25.ts:extractRecordText switches on record.type and only handles the six built-in types (convention, pattern, failure, decision, reference, guide). Custom-type records declared via mulch.config.yaml custom_types (e.g. release_decision, flake_symptom) fall through the switch and contribute only their `tags` field to the BM25 index. Consequence: `ml search` and `searchRecords` have effectively zero recall on custom-type bodies. Fix: when record.type is not one of the six built-ins, consult the type registry (src/registry/type-registry.ts getTypeDefinition) for the type's declared required+optional fields and pass each string/string[] field through the existing addField() helper; if the registry is uninitialized or the type is unknown, fall back to indexing every own enumerable string/string[] field on the record except internal BaseRecord fields already handled (id, classification, recorded_at, archived_at, supersession_demoted_at, anchor_decay_demoted_at, status, owner, archive_reason). Add tests in test/utils/bm25.test.ts that register a custom type via initRegistryFromConfig, build a record with a unique token in its custom field, and assert searchBM25 finds it (current behavior would miss it). Do not change BM25Result shape or DEFAULT_BM25_PARAMS."},{"title":"Fix stale-lock race in withFileLock recovery path","type":"task","priority":2,"labels":["nightwatch"],"blocks":[7],"description":"src/utils/lock.ts:acquireLock has a TOCTOU race in the stale-lock branch (lines ~36-44). After isStaleLock() returns true, the code unconditionally unlinks lockPath and continues. Between the lstat check and the unlink, another process can: (a) unlink the stale lock itself, (b) create a fresh lock, (c) start its critical section — at which point our unlink removes the live owner's lock and we proceed to acquire it concurrently. Fix: capture the mtimeMs returned by isStaleLock and only unlink if a second lstat still reports the same mtime (i.e., the same stale lock instance). If the mtime moved, skip the unlink and fall through to the normal retry+timeout path. Keep the LOCK_STALE_MS / LOCK_RETRY_INTERVAL_MS / LOCK_TIMEOUT_MS constants unchanged. Also gate the stale-lock fast path on `Date.now() < deadline` so a pathological loop cannot overshoot the overall timeout. Add a test in test/utils/ (new file test/utils/lock.test.ts if none exists) that simulates a lock whose mtime advances between observations and asserts withFileLock does NOT unlink it. Do not change the withFileLock signature."},{"title":"Make ml config honor --json for errors","type":"task","priority":3,"labels":["nightwatch"],"blocks":[7],"description":"src/commands/config.ts writes plain-text error messages to stderr (e.g. lines 34-36, 45-48, 50-52, 69-72, 78-80, 89-91) even when the user passed the global --json flag. Every other command in src/commands/ resolves `program.opts().json === true` and calls outputJsonError(, message); config is the lone outlier. Fix: at the top of each `config ` action handler (schema, show, set, unset), compute `const jsonMode = program.opts().json === true;` and replace the existing `process.stderr.write(...)` + `process.exitCode = 1` error paths with `outputJsonError(\"config \", message)` when jsonMode is true (keep the human path otherwise). For `config show` with --json, leave the successful stdout JSON shape unchanged — it already emits machine-readable JSON; only the error envelope needs harmonizing. Add tests in test/commands/config.test.ts asserting that `--json` + a failing path (unknown segment, invalid YAML, missing .mulch/) produces a parseable `{success:false, command:\"config ...\", error}` on stderr and exit code 1."},{"title":"Normalize --version --json envelope to match outputJson","type":"task","priority":3,"labels":["nightwatch"],"blocks":[7],"description":"src/cli.ts:74-83 handles the `--version --json` shortcut by writing `JSON.stringify({name, version, runtime, platform})` (no indentation, no envelope). Every other --json output in the project uses outputJson() which emits `{success: true, command, ...}` indented with 2 spaces. Fix: replace the inline JSON.stringify block with `outputJson({success: true, command: \"version\", name: \"@os-eco/mulch-cli\", version: VERSION, runtime: \"bun\", platform})` so the shape matches. Update test/cli.test.ts (or the existing version test file) to parse the JSON and assert `success === true`, `command === \"version\"`, and the existing fields are still present. Do not change the non-JSON --version output or the VERSION constant itself."},{"title":"Tighten truncate() sentence-boundary short-circuit","type":"task","priority":4,"labels":["nightwatch"],"blocks":[7],"description":"src/utils/format-helpers.ts:63 (`truncate`) returns at the first sentence terminator anywhere in the string as long as `sentenceEnd < maxLen`. With maxLen=100 and input \"OK. Followed by a much longer description that the caller actually wanted in full.\", the helper returns just \"OK.\" — well under maxLen, dropping useful content. The boundary short-circuit should only fire when the boundary is close enough to maxLen to be a meaningful trim. Fix: only return at the sentence boundary when `sentenceEnd >= Math.floor(maxLen / 2)` (or some similar threshold tuned to the existing call sites). Audit current callers (grep `truncate(`) to confirm none rely on the aggressive cut behavior; if any do, switch them to slice manually and keep the new truncate() invariant. Add unit tests in test/utils/ (e.g. test/utils/format-helpers.test.ts, create if absent) covering: text shorter than maxLen, text with no sentence boundary, text with an early sentence boundary, text with a boundary near maxLen. Do not change the truncate() signature or the default maxLen=100."},{"title":"Remove the unused src/utils/index.ts barrel","type":"task","priority":4,"labels":["nightwatch"],"blocks":[7],"description":"knip flags src/utils/index.ts as an unused file. Grep confirms no `from \"../utils/index.ts\"` or `from \"./utils\"` imports anywhere in src/ or test/ — every consumer imports the specific util module directly. The barrel re-exports a subset of utils and drifts out of sync with the actual util surface (e.g. it lists getCurrentVersion/getLatestVersion which still exist but is missing newer utils like anchor-validity, audit, budget, prime-ranking, palette, runtime-flags). Fix: delete src/utils/index.ts. Re-run `bun run lint`, `bun run typecheck`, `bun test`, and `bunx knip` to confirm zero callers. No public API change — the library entry is src/index.ts, which imports util functions directly, not via the barrel."},{"title":"Release: run /release per .claude/commands/release.md","type":"task","priority":3,"labels":["nightwatch"],"description":"After all preceding nightwatch fixes have landed and the gate suite (bun test, bun run lint, bun run typecheck, bun run check:size, bun run check:debt, bun run check:agents) is green on main, run the project's /release command as documented in .claude/commands/release.md to cut a patch release that captures these fixes. Do not bump major or minor — these are correctness/consistency fixes only."}],"acceptance":["bun test still reports 1494 pass / 0 fail (or higher pass count if steps add tests); no test files were deleted.","bun run lint and bun run typecheck exit 0 after every step.","New unit tests in test/utils/bm25.test.ts demonstrate that a custom-type record with a unique token in a type-specific field is returned by searchBM25 for that token.","New unit test in test/utils/lock.test.ts (or equivalent) demonstrates that withFileLock does not unlink a lock whose mtime changed between observations.","Running `ml config set governance.typo 5 --json` (or any failing config path) emits a single parseable {success:false, command, error} JSON object on stderr and exits non-zero; success paths are unchanged.","Running `ml --version --json` emits {success: true, command: 'version', name, version, runtime, platform} indented with 2 spaces; the human-readable --version output is unchanged.","truncate('OK. ' + 'x'.repeat(200), 100) returns ~100 chars, not 'OK.'; existing call sites still render correctly in `ml prime` / `ml status` snapshots.","src/utils/index.ts no longer exists and `bunx knip` no longer reports it (or any new) unused files.","Release step has produced a tagged version bump on npm matching the CHANGELOG entry."]},"children":["mulch-7549","mulch-a517","mulch-6785","mulch-e33d","mulch-c6de","mulch-7612","mulch-6a3a"],"createdAt":"2026-05-31T10:06:47.487Z","updatedAt":"2026-05-31T10:06:47.487Z","name":"nightwatch patrol fixes 2026-05-31"}