Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,34 @@ jobs:
# The hosted demo build must keep compiling too.
- run: pnpm run build:demo

audit:
# `pnpm audit` ran only inside `check:release`, which runs on a tag push
# — so an advisory published by the ecosystem failed a *release* instead
# of a pull request. That is what it cost for v0.5.4: a tag whose
# release did nothing, `Publish to npm` never reached, and the tag moved
# onto the fix rather than the version being burned. The advisory did not
# change; only where it surfaced did. Here a re-run replaces a retag.
#
# Blocking, and at the same threshold as the release gate. A warning
# nobody has to act on is not a gate, and the repository already fixes
# these one way — a `pnpm.overrides` entry — which is a reviewable diff.
# Read what that fix does and does not do in ADR-0021 before adding one.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm audit --audit-level=high
# And the tree a consumer actually resolves, which the line above
# cannot see: root `pnpm.overrides` rewrite resolution in this
# workspace only, so an override silences the gate here and reaches
# no user. Blocking with no allowlist, deliberately — ADR-0021.
- run: pnpm run audit:consumer

smoke:
# Packs the real tarball and installs it in a clean consumer. Only worth
# running once: it is the slowest job and platform-independent.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
---
id: T-0148
title: The release gate fails on a devDependency the package never ships
status: backlog
status: review
type: bug
priority: medium
area: infra
tags: [release, audit, dependencies]
created: 2026-08-03
updated: 2026-08-03
updated: 2026-08-07
scope: [package.json, .github/workflows, scripts/audit-consumer.ts]
related: [ADR-0021, T-0221, T-0220]
---

The `Release` workflow for `v0.5.4` failed at `pnpm run check:release`. Build,
Expand Down Expand Up @@ -49,7 +51,19 @@ because the gate only runs on a tag push. Two options, neither obviously right:

## Acceptance criteria

- [ ] `pnpm audit --audit-level=high` passes on a clean install
- [ ] The decision above is recorded, whichever way it goes
- [x] `pnpm audit --audit-level=high` passes on a clean install
- [x] The decision above is recorded, whichever way it goes
- [ ] If the audit moves into ordinary CI, a pull request proves it fails there
first

## Activity

- 2026-08-07 17:29Z illodev@local#42eb42f5 · claimed
- 2026-08-07 17:39Z illodev@local#42eb42f5 · released

## Notes

- 2026-08-07 17:39Z illodev@local#42eb42f5 — Decided jointly with T-0220 as one policy, recorded in ADR-0021: gates run on pull requests, and local pnpm run check gains neither because it runs constantly and both need the network. pnpm audit --audit-level=high is now its own blocking job in ci.yml at the release gate's threshold. Blocking rather than warning, because check:release would still block at tag time, so a non-blocking pull-request job would be noise people learn to ignore.
Criterion 1 was already met before this card was worked: the four pnpm.overrides make pnpm audit --audit-level=high exit 0 on a clean install, with one moderate remaining (hono via shadcn, below the gate).
And the finding that matters more than the timing question. This card's body reads the workspace/tarball distinction as reassuring — "nothing vulnerable was ever going to reach a user" — and for two of the four overrides it is the opposite. sharp and adm-zip both sit under @huggingface/transformers, which is a dependencies entry of the published @illodev/workfile-search-local. Overrides are a workspace-install mechanism and do not travel inside a published package, so those two entries made this gate green and fixed the advisories for nobody who installed it. Measured rather than reasoned: resolving what the publishable manifests declare, with no overrides, reports four packages at high — sharp <0.35.0 with the libvips CVEs, adm-zip <0.6.0, and onnxruntime-node and transformers through them — all with no fix available, and transformers 4.2.0 is the latest release and pins sharp ^0.34.5. So a third gate exists now that neither card asked for: scripts/audit-consumer.ts audits the tree a consumer resolves, blocking, with no allowlist. That no-allowlist posture is the maintainer's explicit call over a doctor-style baseline, and its consequence is deliberate — the gate is red today and stays red until the published tree changes. Tracked as T-0221 with the routes laid out, because choosing between them is a product decision.
Criterion 3 is unchecked on purpose: the job is wired and the command fails locally with the exit code CI will see, but nothing has run in CI, and this repository does not treat a local run as proof of a pull request. It needs one push.
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
---
id: T-0182
title: check does not exercise the packaged CLI, so a removed flag fails only in CI
status: backlog
status: done
type: task
priority: medium
area: core
created: 2026-08-05
updated: 2026-08-05
updated: 2026-08-07
scope: [packages/workfile/test/cli-callers.test.ts]
related: [LRN-0031, T-0220, T-0158]
verified:
at: "2026-08-07T17:18:17.691Z"
method: local
commit: 4e8da0782fecb7e52899f7916be21ad7f3d4c775
digest: "sha256:dd11bc8e72a0a90347c9acaf3cdd819d86ad1eab09e455f3e3ee7e52e8103c64"
---

`pnpm run check` is what CLAUDE.md tells an agent to run before finishing, and it does not run `smoke:package`. That belongs to `check:release` and to a separate CI job, so the branch that removed `init --language` ([[T-0158]]) was green locally through fourteen commits and failed on the packaged CLI at the first command the smoke test runs.
Expand All @@ -22,5 +29,15 @@ Same class as CI running Windows: the gap is not that the test is missing, it is

## Acceptance criteria

- [ ] A flag removed from the CLI fails a check that `pnpm run check` runs
- [ ] Whichever route is taken names what it still does not cover
- [x] A flag removed from the CLI fails a check that `pnpm run check` runs
- [x] Whichever route is taken names what it still does not cover

## Activity

- 2026-08-07 17:10Z illodev@local#42eb42f5 · claimed
- 2026-08-07 17:18Z illodev@local#42eb42f5 · released

## Notes

- 2026-08-07 17:18Z illodev@local#42eb42f5 — Took the first route, and the survey changed how it was built. Scanning every caller in test/ turns up five flags that are deliberately not real — --bogus, --nonsense, --statuss, and a real flag on the wrong subcommand — because asserting the refusal path is what those tests are for. Rather than carry an allowlist of intentional nonsense, in which a genuinely stale flag could hide, the checker skips the unit tests entirely: a test that sends a removed flag already fails when the suite runs. So it covers exactly the sources pnpm run test does not execute — the generated agent instructions, the docs, scripts/, and test/package-smoke.ts — which is also where the T-0158 failure actually lived. Command-word resolution mirrors commandKey and reads USAGE_ALIASES and DEFAULT_SUBCOMMAND out of the CLI source, which the survey forced: without aliases, workfile docs create --kind in SPEC.md looked like a stale command, and it is a valid alias for doc. An invocation whose word resolves to no row is reported rather than dropped, so the reverse case would surface. The check runs in 52ms, finds 116 invocations, and asserts a floor of 90 so a regex that stops matching fails loudly instead of passing forever. Proven by mutation both halves: removing --scope from the card claim row names 10 sites across .claude, .project/agents, README, SPEC, getting-started and the plugin; removing --yes from init names 3 including package-smoke.ts:215, which is the exact file and command that failed in CI. The largest thing it still does not cover is the other route, and it is named in the test rather than left implicit: this reads text, so check still proves nothing about the packaged artifact — whether the bin ships, whether the shebang survives, whether a consumer can resolve it. Folding smoke:package into check costs ~30s on the command agents run most, which deserves its own decision alongside T-0148 since both are about a gate that only runs on a tag push. Filed as T-0220.
- 2026-08-07 17:18Z illodev@local#42eb42f5 — local verification: pnpm run check green: 465+7 tests pass (1 new), strictNullChecks held at 488. cli-callers.test.ts runs in 52ms inside pnpm run test, finds 116 caller invocations across the generated instructions, docs, scripts and package-smoke.ts, and asserts a coverage floor of 90. Mutation-proven on both extraction halves against an unmodified working tree: dropping --scope from the card claim row fails the test naming 10 caller sites, dropping --yes from init fails naming 3 including packages/workfile/test/package-smoke.ts:215 — the exact command that failed in CI for T-0158 — and bin/workfile.ts restored clean both times. doctor 0/0.
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
---
id: T-0206
title: Two claims held by one resolved actor collide invisibly
status: backlog
status: done
type: bug
priority: medium
area: core
tags: [claims]
effort: S
scope: [packages/workfile/src/modules/cards/claims.ts]
scope: [packages/workfile/src/modules/cards/claims.ts, packages/workfile/src/core/actor.ts, packages/workfile/src/runtime/claude/hooks.mjs, plugins/workfile/runtime/hooks.mjs, packages/workfile/test/claims.test.ts, packages/workfile/test/claude-surface.test.ts, packages/workfile/ui/src/types.ts]
origin: [T-0196]
created: 2026-08-05
updated: 2026-08-05
updated: 2026-08-07
related: [ADR-0020, LRN-0030, T-0219, T-0196]
verified:
at: "2026-08-07T17:02:17.071Z"
method: local
commit: 4e8da0782fecb7e52899f7916be21ad7f3d4c775
digest: "sha256:32694851285580f3877b44d3ff44fcf51cff487cf052e8131da85045701783d8"
---

`activity.conflicts` pairs claimed cards whose scopes overlap and skips pairs
Expand All @@ -35,7 +41,18 @@ Surfaced by T-0196, whose popover can only report the conflicts it is handed.

## Acceptance criteria

- [ ] Two claims from different sessions with overlapping scopes are reported as a conflict whatever their actors resolve to.
- [ ] A single session moving between its own overlapping cards is still not a conflict.
- [ ] The rule for two claims with no session is decided, recorded and tested.
- [ ] The claim board the scope guard reads applies the same rule as the activity snapshot.
- [x] Two claims from different sessions with overlapping scopes are reported as a conflict whatever their actors resolve to.
- [x] A single session moving between its own overlapping cards is still not a conflict.
- [x] The rule for two claims with no session is decided, recorded and tested.
- [x] The claim board the scope guard reads applies the same rule as the activity snapshot.

## Activity

- 2026-08-07 16:52Z illodev@local#42eb42f5 · claimed
- 2026-08-07 17:02Z illodev@local#42eb42f5 · released

## Notes

- 2026-08-07 17:02Z illodev@local#42eb42f5 — Fixed. The skip compared actors, which reads as a session check only because resolveActor writes the discriminator into the actor's tail — so it was right often enough to look correct and wrong exactly where it mattered. claimSeparation now answers "provably the same process" and names its evidence: sessions-differ, actors-differ, or unproven. A session is recovered from either place that carries it, the session file or the actor's tail, both normalised through sessionDiscriminator, now exported from core/actor.ts so there is one definition instead of two that can drift. Found a second defect feeding the same rule: claimState resolved a claim's session with one find over `cardId === card.id || actor === claimed_by`, so two cards held by one actor string could both be attributed to whichever session came first — erasing the exact evidence the comparison needs. Attribution now prefers the session that names the card. The decision on the sessionless case is ADR-0020: unproven is reported, not dropped and not prompted on. Dropping it is the bug that let two terminals collide with no trace; prompting on it interrupts somebody about a card they claimed themselves, which is the guard people switch off. So the snapshot carries it with its basis and the scope guard stays silent on it. Criterion 4 turned out not to need a behaviour change, and that is the finding rather than a shortcut: because the session lives in the actor's tail, the guard's string comparison IS the session comparison for every pairing it can see. Rather than argue that, separatesFromMe names it and a new pin drives the real hook over six pairings and requires its silence to match claimSeparation — verified by mutation, flipping the comparison to `return true` fails four of the six with the diagnostic naming both verdicts, and restoring it goes green. One asymmetry stays and is recorded rather than half-fixed: the snapshot can read a session from a session file, the guard cannot, because board.json carries no session and the hook imports nothing from the package on a p95-under-30ms budget. So two agents sharing an explicit --actor are a reported conflict the guard will not prompt about — LRN-0030 and T-0219.
- 2026-08-07 17:02Z illodev@local#42eb42f5 — local verification: pnpm run check green: 464+7 tests pass (4 new), strictNullChecks held at 488. New rule tests cover the session table, two terminals sharing an actor reported as unproven, one actor string over two sessions reported as sessions-differ, and one session over two overlapping cards reported as no conflict. Guard pin drives the real PreToolUse hook over six pairings against claimSeparation; mutation-checked by flipping separatesFromMe to , which fails 4 of 6, and restoring it returns 19/19. Plugin runtime regenerated so both copies match. doctor 0/0, memory verify 0/0.
- 2026-08-07 17:02Z illodev@local#42eb42f5 — Correction to the verification entry above: a shell substitution ate two words from it. The mutation check flipped separatesFromMe to return true — that is what failed 4 of the 6 guard pairings before restoring it returned 19/19.
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
---
id: T-0213
title: doctor calls a healthCheck the repository declares, on every runner
status: backlog
status: done
type: audit
priority: high
area: infra
tags: [security]
effort: S
scope: [packages/workfile/src/modules/integrations]
scope: [packages/workfile/src/modules/integrations, packages/workfile/src/modules/health, packages/workfile/src/modules/ci, packages/workfile/test/integrations.test.ts, packages/workfile/test/ci-targets.test.ts, packages/workfile/strict-baseline.json]
origin: [T-0188, LRN-0028]
created: 2026-08-05
updated: 2026-08-05
updated: 2026-08-07
related: [ADR-0019, LRN-0029, T-0218]
verified:
at: "2026-08-07T16:48:16.725Z"
method: local
commit: 4e8da0782fecb7e52899f7916be21ad7f3d4c775
digest: "sha256:0d50a6371d79d42b7d4bd6d0ac6b1ec47f552ff820f2f7ebc6708756b7be3d0f"
---

Found while auditing T-0188, and one hop further out than LRN-0025 recorded.
Expand Down Expand Up @@ -47,7 +53,17 @@ What to establish, in order:

## Acceptance criteria

- [ ] Every surface that calls repository-declared code is listed, with where it runs.
- [ ] A decision is recorded on whether `doctor` calls a declared `healthCheck`, with the reason.
- [ ] The generated CI documentation states what a repository's own config can execute, rather than leaving it to be discovered.
- [ ] If the behaviour changes, a project that declares no integrations behaves exactly as before, proven by a test.
- [x] Every surface that calls repository-declared code is listed, with where it runs.
- [x] A decision is recorded on whether `doctor` calls a declared `healthCheck`, with the reason.
- [x] The generated CI documentation states what a repository's own config can execute, rather than leaving it to be discovered.
- [x] If the behaviour changes, a project that declares no integrations behaves exactly as before, proven by a test.

## Activity

- 2026-08-07 16:32Z illodev@local#42eb42f5 · claimed
- 2026-08-07 16:48Z illodev@local#42eb42f5 · released

## Notes

- 2026-08-07 16:48Z illodev@local#42eb42f5 — Audited and closed. The premise the card was filed on holds — runDoctor does hand doctor a repository-supplied function and call it — but the conclusion does not: removing the call closes nothing, because loadWorkspace already import()s project.config.mjs in every command, so the module body runs earlier and more quietly than any hook. LRN-0029 lists all three channels with their callers; ADR-0019 records the decision. What the audit actually found was a robustness defect, verified against the packaged CLI on a scratch workspace: a healthCheck that threw propagated out of runDoctor and took the command down with a raw Error, so the report that would have named the broken integration never printed — the CLI, /api/v2/health, /api/health and both MCP doctor surfaces all died with it. A hook returning { severity: "catastrophe" } wrote NaN into doctor's counts, landed in no bucket, left ok true, and made the severity comparator sort on NaN — an integration could decide whether the repository passed, by typo. And a hook that never settled had no bound at all. Now each call is isolated, bounded at 10s and validated: real binary, two hooks declared, reports integration-health-check-failed naming "no credentials configured" and integration-health-check-invalid naming "1 of 1 diagnostics that could not be counted", both error, ok: false, exit 1. Hanging hook at the real default: integration-health-check-timeout with timeoutMs 10000 after 11s wall clock, report printed. No integrations declared: 0 errors, 0 warnings, zero integration findings. The bound is partial on purpose and the code says so — a hook runs on doctor's own event loop, so it catches an awaited hang and cannot catch a synchronous spin. One trap worth the record: the timer was unref'd in the first draft, which let Node exit before the bound fired and printed no report at all — strictly worse than the hang. Filed T-0218 for the provenance gap the audit surfaced: doctor flattens away the module that produced an issue, so a well-formed diagnostic from a repository's own hook is indistinguishable from one Workfile wrote.
- 2026-08-07 16:48Z illodev@local#42eb42f5 — local verification: pnpm run check green: 460+7 tests pass, strictNullChecks held at 488 with registry.ts ratcheted 10 to 9. Packaged CLI against scratch workspaces: throwing and malformed hooks reported as integration-health-check-failed and -invalid at error severity with ok false and exit 1 instead of crashing runDoctor; hanging hook bounded at the 10s default (timeoutMs 10000, 11s wall clock, report printed); a workspace declaring no integrations reports 0 errors, 0 warnings and zero integration findings. Repo doctor 0/0, memory verify 0/0.
Loading