diff --git a/packages/tbd/CHANGELOG.md b/packages/tbd/CHANGELOG.md index 267f456b..354e22c1 100644 --- a/packages/tbd/CHANGELOG.md +++ b/packages/tbd/CHANGELOG.md @@ -1,5 +1,131 @@ # get-tbd +## 0.8.1 + +A patch release about what an agent reads before it touches your code. +Rust becomes a first-class guideline family, four language-neutral topics that were +previously scattered across other documents get their own, and the set an agent is told +to read for *any* engineering work drops from nine documents to one. +No format change: repositories stay on `f08`, and there is no migration step. + +### Rust is a first-class guideline family + +Eight documents, reachable the same way as the TypeScript and Python families: + +- `rust-rules` — general coding rules for libraries, applications, services, and CLIs +- `rust-project-setup` — structuring, validating, and maintaining packages and + workspaces +- `rust-lint-format-rules` — the `[lints]` block, `clippy.toml`, rustfmt and toolchain + pinning, hooks and CI gates, and how to prove the floor is live +- `rust-testing-rules` — unit, integration, property, snapshot, and cross-platform tests +- `rust-cli-rules` — composable, testable, cross-platform command-line applications +- `rust-filesystem-rules` — path and string types, write boundaries, and the `tempfile` + atomic-replacement sequence +- `rust-code-review-rules` — which guideline owns each changed surface, plus the + `unsafe` and FFI checklist +- `rust-release-rules` — crates.io publishing and workspace ordering, trusted + publishing, semver checks, and shipping a Rust binary as a Python wheel through + maturin + +```bash +tbd guidelines rust-rules rust-lint-format-rules +tbd shortcut review-code-rust +``` + +`rust-lint-format-rules` reports measured adoption cost for the lints above the floor, +taken from a real 35k-line codebase, so a project can choose its lint set from evidence +rather than taste. `check-rust-gate.mjs` ships beside the guidelines so a project can +check that the floor it declares is the floor it actually runs. + +`rust` is now a doc category, so `tbd docs fork --category=rust` forks the family and +the generated agent surfaces list it under its own heading. +`tbd shortcut review-code` routes a Rust diff to the Rust documents. + +### Four cross-cutting topics that used to be implicit + +Each is language-neutral and pairs with the language-specific document where one exists: + +- `code-review-rules` — the Blocker/High/Medium/Low severity vocabulary, establishing a + baseline before hunting findings, reviewing the highest-risk boundaries first, and + what makes a finding actionable. + The `review-code` shortcuts are the procedure; this is the substance they apply. +- `ci-and-gates-rules` — one entry point in two modes, thin workflow orchestration over + tested project-native programs, and the traps that keep a gate green while it checks + nothing: swallowed pipeline exit status, self-recorded evidence, single-platform + blindness, and scope holes. +- `filesystem-rules` — atomic publication of finished files, atomic visibility versus + crash durability, cross-device moves, deterministic traversal, symlink and root + boundaries, and honest partial failure. +- `release-engineering-rules` — one release identity, a pre-release gate that runs where + publishing happens, least-privilege publishing authority, build-once-and-promote, and + smoke-testing the packaged artifact rather than the build output. + +### The always-read core is one document, not nine + +The generated guideline directory sorted documents into groups, and the first group told +agents to “read all of these for any engineering work.” +That group had grown to nine documents and roughly 10,600 words — read in full before +the agent had seen a line of your code, competing directly with attention for your +actual repository. + +It is now one document, `general-eng-agent-principles`, at about 1,100 words. +Everything else is routed by what the change touches, under a new **Cross-cutting +engineering topics** group and the per-language groups, each of which now says to select +from it rather than load it whole. + +Group membership is matched on exact names instead of substrings. +The old rule matched any name containing `testing`, `tdd`, or `golden`, so the Rust +family arriving in this release would have pulled `rust-testing-rules` into the group +every agent reads for every task, in any language. +A test now measures what the always-read core renders to and fails if it grows. + +### A push failure is classified by the error, not by stray digits + +`tbd sync` classifies a failed push as permanent, transient, or unknown, and acts on +that: a permanent failure auto-saves your changes to the outbox and reports the push as +blocked, while the other two suggest a retry. +The HTTP status patterns it classified with (`403`, `401`, `5\d\d`) matched bare digits +anywhere in the error text, including the repository path. + +So a push failure under a directory like `/tmp/build-05895p` read as an HTTP 5xx server +error, because `5\d\d` matched the `589` in that name — and the identical failure in a +differently-named directory classified as `unknown`. A path containing `403` was worse: +it made an ordinary network failure look policy-blocked and triggered the outbox save. + +Status codes are now recognized only where git actually reports one — `HTTP 500`, +`returned error: 403`, `status=401` — and not when the digits are embedded in a path, +object id, or larger number. + +This surfaced during the release validation for this version: the same sync test +classified its failure differently between runs, purely on the random temp directory +name it happened to get. + +### Guidelines and content + +- `general-testing-rules` grew from a stub into a full document: keeping suites concise + and portable, not just testing the test, asserting transferred data rather than that a + mock was called, keeping the inner loop fast, raising a timeout only with a recorded + measurement, and never letting an absent test look like a passing one. +- `typescript-rules` replaces its general file-operations section with an explicit rule + to atomically publish files completed in one operation. +- `release-notes-guidelines` adds the rule that a defect introduced and corrected before + a release is not a shipped fix, with the cross-check for finding them. +- `new-guideline`, `review-code`, `review-code-typescript`, and `review-code-python` + were updated for the new routing; `supply-chain-hardening`, + `golden-testing-guidelines`, `error-handling-rules`, `typescript-cli-tool-rules`, + `typescript-lint-format-rules`, `python-rules`, and `python-modern-guidelines` + received focused corrections. + +The bundled guideline count goes from 31 to 43. + +### Security + +Lockfile byte-identical since v0.8.0, so the installed dependency tree is unchanged. +`pnpm audit --prod` reports no runtime advisories. +The 32 open advisories are all dev-only, reached through `vitest`/`vite` and other build +tooling that is not shipped to users; they are tracked in the repository rather than +blocking this release. + ## 0.8.0 tbd could say that work was open, started, or closed. diff --git a/packages/tbd/package.json b/packages/tbd/package.json index 17855c2d..2302eaed 100644 --- a/packages/tbd/package.json +++ b/packages/tbd/package.json @@ -1,6 +1,6 @@ { "name": "get-tbd", - "version": "0.8.0", + "version": "0.8.1", "description": "Git-native issue tracking for AI agents and humans", "license": "MIT", "author": "Joshua Levy (https://github.com/jlevy)", diff --git a/packages/tbd/src/cli/lib/errors.ts b/packages/tbd/src/cli/lib/errors.ts index e7f82279..93a89920 100644 --- a/packages/tbd/src/cli/lib/errors.ts +++ b/packages/tbd/src/cli/lib/errors.ts @@ -132,16 +132,32 @@ export type SyncErrorType = 'permanent' | 'transient' | 'unknown'; * @param error - Error message or Error object from git push * @returns Classification of the error type */ +/** + * Match an HTTP status code only where the message reports one. + * + * A bare `/5\d\d/` matches digits anywhere, so any path, object id, or byte count + * carrying those characters decides the classification. That is not theoretical: a + * missing-remote push failure under `/tmp/tryscript-05895p` classified as a 5xx + * server error because `5\d\d` matched the `589` in the directory name, while the + * identical failure elsewhere classified as `unknown`. + * + * A status code git reports stands on its own — `HTTP 500`, `returned error: 403`, + * `status=401` — so require a boundary on both sides. The two sides differ: a + * leading `.` means the digits are part of a longer number (`v1.500.2`), while a + * trailing `.` is ordinary sentence punctuation (`returned error: 503.`). + */ +function reportsStatusCode(lowerMessage: string, code: string): boolean { + return new RegExp(String.raw`(? reportsStatusCode(lower, code))) { + return 'permanent'; + } + for (const pattern of permanentPatterns) { if (pattern.test(lower)) { return 'permanent'; @@ -164,7 +185,6 @@ export function classifySyncError(error: Error | string): SyncErrorType { /connection reset/, /network/, /dns/, - /5\d\d/, // HTTP 5xx server errors /server error/, /temporarily/, /try again/, @@ -173,6 +193,11 @@ export function classifySyncError(error: Error | string): SyncErrorType { /connection closed/, ]; + // HTTP 5xx server errors + if (reportsStatusCode(lower, String.raw`5\d\d`)) { + return 'transient'; + } + for (const pattern of transientPatterns) { if (pattern.test(lower)) { return 'transient'; diff --git a/packages/tbd/tests/errors.test.ts b/packages/tbd/tests/errors.test.ts index e36631dd..9df5ac9f 100644 --- a/packages/tbd/tests/errors.test.ts +++ b/packages/tbd/tests/errors.test.ts @@ -210,6 +210,37 @@ describe('classifySyncError', () => { }); }); + describe('status codes are read as status codes, not as stray digits', () => { + // A status pattern that matches bare digits anywhere reclassifies a failure + // based on characters that carry no meaning: a temp directory, a SHA, a byte + // count. This is not hypothetical — a missing-remote push failure under a + // directory named `tryscript-05895p` was classified `transient` because + // `5\d\d` matched the `589` inside it, while the same failure elsewhere was + // `unknown`. The classification decides whether tbd auto-saves to the outbox + // or tells the user to retry, so it must depend only on the error. + it('ignores status-code digits embedded in a path', () => { + const missingRemote = + 'Command failed: git -C /tmp/tryscript-05895p push --no-verify origin ' + + 'refs/heads/tbd-sync:refs/heads/tbd-sync\n' + + "fatal: 'origin' does not appear to be a git repository"; + expect(classifySyncError(missingRemote)).toBe('unknown'); + }); + + it('ignores status-code digits inside longer numbers and identifiers', () => { + expect(classifySyncError('wrote 4031 objects')).toBe('unknown'); + expect(classifySyncError('object 5008f2a is missing')).toBe('unknown'); + expect(classifySyncError('/tmp/build-401k/repo is not a git repository')).toBe('unknown'); + expect(classifySyncError('/var/data/x-500-y is missing')).toBe('unknown'); + }); + + it('still reads real status codes in the shapes git reports them', () => { + expect(classifySyncError('The requested URL returned error: 403')).toBe('permanent'); + expect(classifySyncError('error: RPC failed; HTTP 500 curl 22')).toBe('transient'); + expect(classifySyncError('The requested URL returned error: 503.')).toBe('transient'); + expect(classifySyncError('status=401, push rejected')).toBe('permanent'); + }); + }); + describe('unknown errors', () => { it('classifies ambiguous errors as unknown', () => { expect(classifySyncError('Something went wrong')).toBe('unknown');