From 92911b2e0cc4ddfd6bafee977cca369bab7e165c Mon Sep 17 00:00:00 2001 From: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:02:23 -0700 Subject: [PATCH 01/10] fix(cli): speed up standalone version queries (#34) * perf(cli): answer --version through the axi-sdk-js fast path Extract the package-version helper out of the heavy `src/cli.ts` graph into a leaf `src/version.ts` (node builtins only), and rewrite `bin/tasks-axi.ts` to answer a bare `-v`/`-V`/`--version` via `axi-sdk-js/fast-path`, dynamically importing the command graph only for everything else. Bumps axi-sdk-js to ^0.1.10 for the `./fast-path` subpath export. Version output is byte-identical and all other argv shapes still route through `runAxiCli` unchanged. Guarded by a deterministic ESM loader module trace with a negative control plus flag parity; no wall-clock assertion in CI. * no-mistakes(document): Confirm fast-path docs and lint cleanliness * no-mistakes: apply CI fixes --- .../cli-transcript.txt | 29 +++++ .../version-module-trace.txt | 3 + AGENTS.md | 7 ++ bin/tasks-axi.ts | 8 +- package.json | 2 +- pnpm-lock.yaml | 10 +- src/cli.ts | 30 +---- src/version.ts | 36 ++++++ test/bin/version-fast-path.test.ts | 108 ++++++++++++++++++ test/fixtures/module-trace-hook.mjs | 16 +++ test/fixtures/module-trace-register.mjs | 7 ++ 11 files changed, 219 insertions(+), 37 deletions(-) create mode 100644 .no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/cli-transcript.txt create mode 100644 .no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/version-module-trace.txt create mode 100644 src/version.ts create mode 100644 test/bin/version-fast-path.test.ts create mode 100644 test/fixtures/module-trace-hook.mjs create mode 100644 test/fixtures/module-trace-register.mjs diff --git a/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/cli-transcript.txt b/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/cli-transcript.txt new file mode 100644 index 0000000..ce35ba5 --- /dev/null +++ b/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/cli-transcript.txt @@ -0,0 +1,29 @@ +End-user CLI verification + +$ node --import tsx bin/tasks-axi.ts -v +0.2.4 +exit: 0 + +$ node --import tsx bin/tasks-axi.ts -V +0.2.4 +exit: 0 + +$ node --import tsx bin/tasks-axi.ts --version +0.2.4 +exit: 0 + +$ node --import tsx bin/tasks-axi.ts list --version +error: "Unknown flag: --version" +code: VALIDATION_ERROR +help[1]: Run the command with --help to see supported flags +exit: 2 + +Module-trace verification for the bare --version invocation + +present: src/version.ts +present: axi-sdk-js/dist/fast-path.js +absent: src/cli.ts +absent: @toon-format modules +absent: axi-sdk-js/dist/index.js + +The complete loader-produced trace is in version-module-trace.txt. diff --git a/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/version-module-trace.txt b/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/version-module-trace.txt new file mode 100644 index 0000000..1a85011 --- /dev/null +++ b/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/version-module-trace.txt @@ -0,0 +1,3 @@ +file:///Users/kunchen/.no-mistakes/worktrees/4ebfb4ced0b4/01KZD5GTT42WND2KE5SZK4D5Y5/bin/tasks-axi.ts +file:///Users/kunchen/.no-mistakes/worktrees/4ebfb4ced0b4/01KZD5GTT42WND2KE5SZK4D5Y5/node_modules/.pnpm/axi-sdk-js@0.1.10/node_modules/axi-sdk-js/dist/fast-path.js +file:///Users/kunchen/.no-mistakes/worktrees/4ebfb4ced0b4/01KZD5GTT42WND2KE5SZK4D5Y5/src/version.ts diff --git a/AGENTS.md b/AGENTS.md index 73cf843..c577e80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,6 +66,13 @@ The CLI layer never knows which backend is active — it only talks to the `Stor This lets an agent confirm a write deterministically without a follow-up read. Errors still use structured-error output + non-zero exit (not JSON), so `exit 0` + `ok:true` = success. +## Entry point & the `--version` fast path + +`bin/tasks-axi.ts` answers a bare `-v`/`-V`/`--version` through `axi-sdk-js/fast-path` and only then `await import`s `src/cli.js`, so the heavy command graph never loads for a version query (~31ms -> ~20ms, the node floor). +That makes `src/version.ts` a **leaf**: it may import node builtins and nothing else - importing anything from the command graph silently destroys the speedup. `cli.ts` re-imports `VERSION` from it, so there is still one source of the version string. +Any argv shape other than exactly one version flag falls through to `runAxiCli`, which remains the sole owner of the general case (e.g. `list --version` is still an unknown-flag error). +`test/bin/version-fast-path.test.ts` guards this deterministically with an ESM loader module trace (`test/fixtures/module-trace-*.mjs`) plus a negative control; do **not** add a wall-clock timing assertion to CI - it is flaky under runner contention. + ## Build / test / ship - `pnpm build` (tsc), `pnpm test` (vitest, `test/` mirrors `src/`), `pnpm lint` (eslint), `pnpm run build:skill -- --check` (the generated `skills/tasks-axi/SKILL.md` is built from `DESCRIPTION` + `TOP_HELP` and must not drift — CI runs the check). diff --git a/bin/tasks-axi.ts b/bin/tasks-axi.ts index 6068881..4cbc0b5 100644 --- a/bin/tasks-axi.ts +++ b/bin/tasks-axi.ts @@ -1,4 +1,8 @@ #!/usr/bin/env node -import { main } from "../src/cli.js"; +import { tryFastPath } from "axi-sdk-js/fast-path"; +import { VERSION } from "../src/version.js"; -main(); +if (!tryFastPath(process.argv.slice(2), { version: VERSION })) { + const { main } = await import("../src/cli.js"); + await main(); +} diff --git a/package.json b/package.json index c2e4fa0..b97ea40 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@toon-format/toon": "^2.1.0", - "axi-sdk-js": "^0.1.7" + "axi-sdk-js": "^0.1.10" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1be2d3..d303bbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^2.1.0 version: 2.3.0 axi-sdk-js: - specifier: ^0.1.7 - version: 0.1.7 + specifier: ^0.1.10 + version: 0.1.10 devDependencies: '@eslint/js': specifier: ^10.0.1 @@ -686,8 +686,8 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - axi-sdk-js@0.1.7: - resolution: {integrity: sha512-kzIeuQHZx3FFpzJknq+sdbi3XBGdyPGMdOHM2Zepa4MF5MpckhPeWPd5x+RX6gBKEWZdaW73YrOPY+HAGyLekw==} + axi-sdk-js@0.1.10: + resolution: {integrity: sha512-mktHOya6qUgqDcMpmj5WsNDzArre15N2qfns8bY98nrHGTSqdBnH2Ok77c2zOA2jYbGHO/BhhVZtDGK/UTO70g==} engines: {node: '>=20'} balanced-match@4.0.4: @@ -1597,7 +1597,7 @@ snapshots: assertion-error@2.0.1: {} - axi-sdk-js@0.1.7: + axi-sdk-js@0.1.10: dependencies: '@toon-format/toon': 2.3.0 diff --git a/src/cli.ts b/src/cli.ts index 62f229b..7f37a43 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,3 @@ -import { existsSync, readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; import { runAxiCli } from "axi-sdk-js"; import { requireFlagValue, @@ -53,12 +50,11 @@ import { } from "./commands/public-followup.js"; import { SETUP_HELP, setupCommand } from "./commands/setup.js"; import type { SuggestionGlobals } from "./suggestions.js"; +import { VERSION } from "./version.js"; export const DESCRIPTION = "Agent ergonomic task & backlog manager for the current workspace. Prefer this over hand-editing backlog.md for task state, dependency, or hold changes."; -const VERSION = readPackageVersion(); - type CliStdout = Pick; type MainOptions = { @@ -238,27 +234,3 @@ function parseGlobalFlags(args: string[]): { function requireNonEmptyGlobalFlagValue(flag: string, value: string): string { return requireNonEmptySingleLineFlagValue(flag, value) ?? value; } - -function readPackageVersion(): string { - const here = dirname(fileURLToPath(import.meta.url)); - - for (const candidate of [ - join(here, "..", "package.json"), - join(here, "..", "..", "package.json"), - ]) { - if (!existsSync(candidate)) continue; - const parsed = JSON.parse(readFileSync(candidate, "utf-8")) as { - version?: unknown; - name?: unknown; - }; - if ( - parsed.name === "tasks-axi" && - typeof parsed.version === "string" && - parsed.version.length > 0 - ) { - return parsed.version; - } - } - - throw new Error("Could not determine tasks-axi package version"); -} diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..f50afe4 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,36 @@ +/** + * Leaf module: the package version, resolved from `package.json`. + * + * This file must import ONLY node builtins. `bin/tasks-axi.ts` imports it + * eagerly so `--version` can be answered before the heavy command graph in + * `cli.ts` is dynamically imported. + */ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +function readPackageVersion(): string { + const here = dirname(fileURLToPath(import.meta.url)); + + for (const candidate of [ + join(here, "..", "package.json"), + join(here, "..", "..", "package.json"), + ]) { + if (!existsSync(candidate)) continue; + const parsed = JSON.parse(readFileSync(candidate, "utf-8")) as { + version?: unknown; + name?: unknown; + }; + if ( + parsed.name === "tasks-axi" && + typeof parsed.version === "string" && + parsed.version.length > 0 + ) { + return parsed.version; + } + } + + throw new Error("Could not determine tasks-axi package version"); +} + +export const VERSION = readPackageVersion(); diff --git a/test/bin/version-fast-path.test.ts b/test/bin/version-fast-path.test.ts new file mode 100644 index 0000000..e0ab758 --- /dev/null +++ b/test/bin/version-fast-path.test.ts @@ -0,0 +1,108 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +/** + * `bin/tasks-axi.ts` answers a bare version flag through + * `axi-sdk-js/fast-path` + the leaf `src/version.ts`, and only dynamically + * imports `src/cli.ts` for everything else. These guards are deterministic: + * they assert the module graph actually loaded, not wall-clock time. + */ + +const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url)); +const BIN = join(REPO_ROOT, "bin", "tasks-axi.ts"); +const REGISTER = new URL( + "../fixtures/module-trace-register.mjs", + import.meta.url, +).href; + +const { version: PKG_VERSION } = JSON.parse( + readFileSync(join(REPO_ROOT, "package.json"), "utf8"), +) as { version: string }; + +let traceDir: string; +let nextTrace = 0; + +beforeAll(() => { + traceDir = mkdtempSync(join(tmpdir(), "tasks-axi-trace-")); +}); + +afterAll(() => { + rmSync(traceDir, { recursive: true, force: true }); +}); + +interface Run { + stdout: string; + status: number | null; + loaded: string[]; +} + +function run(args: string[]): Run { + const tracePath = join(traceDir, `trace-${nextTrace++}.txt`); + const result = spawnSync( + process.execPath, + ["--import", "tsx", "--import", REGISTER, BIN, ...args], + { + cwd: REPO_ROOT, + encoding: "utf8", + env: { ...process.env, AXI_MODULE_TRACE: tracePath }, + }, + ); + + const trace = readFileSync(tracePath, "utf8"); + return { + stdout: result.stdout, + status: result.status, + loaded: trace.split("\n").filter((line) => line.length > 0), + }; +} + +const loadedHeavyGraph = (loaded: string[]): boolean => + loaded.some((url) => url.endsWith("/src/cli.ts")); + +describe("version fast path", () => { + it.each(["-v", "-V", "--version"])( + "prints exactly the package version for %s and skips the command graph", + (flag) => { + const { stdout, status, loaded } = run([flag]); + + expect(stdout).toBe(`${PKG_VERSION}\n`); + expect(status).toBe(0); + + // Only the bin, the SDK fast path, and the leaf version module load. + expect(loadedHeavyGraph(loaded)).toBe(false); + expect( + loaded.filter((url) => url.includes("/@toon-format/")), + ).toHaveLength(0); + expect( + loaded.filter((url) => url.endsWith("/axi-sdk-js/dist/index.js")), + ).toHaveLength(0); + expect(loaded.some((url) => url.endsWith("/src/version.ts"))).toBe(true); + expect(loaded.some((url) => url.endsWith("/fast-path.js"))).toBe(true); + }, + ); + + // Negative control: the probe above is only meaningful if it would notice + // the heavy graph being loaded. These argv shapes deliberately fall through + // to `runAxiCli`, and the trace must show it. + it.each([["--help"], ["list", "--help"], ["list", "--version"]])( + "still loads the command graph for %s", + (...args: string[]) => { + const { loaded } = run(args); + + expect(loadedHeavyGraph(loaded)).toBe(true); + }, + ); + + it("leaves a trailing version flag to the full CLI, unchanged", () => { + // Pre-change behaviour: `runAxiCli` owns the general case and rejects a + // version flag in a command position. The fast path must not swallow it. + const { stdout, status } = run(["list", "--version"]); + + expect(stdout).toContain('error: "Unknown flag: --version"'); + expect(status).not.toBe(0); + }); +}); diff --git a/test/fixtures/module-trace-hook.mjs b/test/fixtures/module-trace-hook.mjs new file mode 100644 index 0000000..585756b --- /dev/null +++ b/test/fixtures/module-trace-hook.mjs @@ -0,0 +1,16 @@ +// ESM loader hook: appends the URL of every module the process loads to the +// file named by the registration `data.out`. Used by +// `test/bin/version-fast-path.test.ts` to prove the heavy command graph is not +// loaded on the `--version` fast path. +import { appendFileSync } from "node:fs"; + +let out; + +export function initialize(data) { + out = data.out; +} + +export async function load(url, context, next) { + if (out) appendFileSync(out, `${url}\n`); + return next(url, context); +} diff --git a/test/fixtures/module-trace-register.mjs b/test/fixtures/module-trace-register.mjs new file mode 100644 index 0000000..e448fe1 --- /dev/null +++ b/test/fixtures/module-trace-register.mjs @@ -0,0 +1,7 @@ +// Registers `module-trace-hook.mjs` for the process it is `--import`ed into. +// The trace destination comes from `AXI_MODULE_TRACE`. +import { register } from "node:module"; + +register(new URL("./module-trace-hook.mjs", import.meta.url).href, { + data: { out: process.env.AXI_MODULE_TRACE }, +}); From 44a41f38f3c74e5337c10b88ca2e1555374c6af4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:10:39 -0700 Subject: [PATCH 02/10] chore(main): release tasks-axi 0.2.5 (#35) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 69535c6..461d94a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.4" + ".": "0.2.5" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a895b..414dd6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.5](https://github.com/kunchenguid/tasks-axi/compare/tasks-axi-v0.2.4...tasks-axi-v0.2.5) (2026-08-07) + + +### Bug Fixes + +* **cli:** speed up standalone version queries ([#34](https://github.com/kunchenguid/tasks-axi/issues/34)) ([92911b2](https://github.com/kunchenguid/tasks-axi/commit/92911b2e0cc4ddfd6bafee977cca369bab7e165c)) + ## [0.2.4](https://github.com/kunchenguid/tasks-axi/compare/tasks-axi-v0.2.3...tasks-axi-v0.2.4) (2026-07-23) diff --git a/package.json b/package.json index b97ea40..d297815 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tasks-axi", - "version": "0.2.4", + "version": "0.2.5", "packageManager": "pnpm@11.1.1", "description": "AXI-compliant task/backlog CLI — token-efficient TOON output, pluggable backends, byte-exact markdown round-trip, idempotent mutations", "type": "module", From 9a86c7c86a4617a5a4f00f28dcb9588b03897f8f Mon Sep 17 00:00:00 2001 From: Evelyn Scidmore <13389701+escidmore@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:31:53 -0700 Subject: [PATCH 03/10] fix(cli): accept canonical Forgejo pull request URLs (#36) * fix: accept canonical Forgejo pull request URLs as typed PR links One classification seam (isPrUrl in src/pr-url.ts) now decides what counts as a PR URL for prose link derivation, done/add --pr validation, and public-followup pr_url deliverables: canonical GitHub https://github.com///pull/ or Forgejo https://///pulls/ with a positive, no-leading-zero number. Near misses (issue routes, singular/plural route confusion, trailing slash, query/fragment, whitespace, userinfo, ports, encoded separators, malformed segments) are rejected as --pr / pr_url values and derive as doc links, never pr. Fixes #19 * fix: validate pr links against the untrimmed input Review follow-ups from pipeline run 01KZFA73D662DCSJ4HVWKW21QX: --pr values and pr-kind addLinks are validated before any trim, so whitespace-padded input is rejected instead of normalized. The literal NUL byte in test/pr-url.test.ts is now written as a unicode source escape so git treats the file as text; an embedded-space rejection case is added alongside it. --- AGENTS.md | 1 + README.md | 1 + src/backends/markdown-grammar.ts | 8 ++-- src/backends/markdown.ts | 7 +++- src/commands/crud.ts | 8 ++-- src/commands/state.ts | 1 + src/pr-url.ts | 32 ++++++++++++++++ src/public-followup.ts | 12 +----- test/backends/markdown-grammar.test.ts | 36 ++++++++++++++++++ test/backends/markdown.test.ts | 5 +++ test/commands/public-followup.test.ts | 42 +++++++++++++++++++++ test/commands/state.test.ts | 44 ++++++++++++++++++++++ test/pr-url.test.ts | 52 ++++++++++++++++++++++++++ 13 files changed, 230 insertions(+), 19 deletions(-) create mode 100644 src/pr-url.ts create mode 100644 test/pr-url.test.ts diff --git a/AGENTS.md b/AGENTS.md index c577e80..30ac55e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ The CLI layer never knows which backend is active — it only talks to the `Stor - `src/context.ts` — `resolveTasksContext` builds the backend `Store` + `ResolvedConfig`; every command receives this `TasksContext`. - `src/store.ts` - the `Store` interface and `Capabilities`. Core contract: `create/get/update/remove/list/transition/addDep/removeDep/updatePublicFollowup`. `prune`/`render` are optional and capability-gated. - `src/model.ts` — the `Task` data model (report §5). +- `src/pr-url.ts` — `isPrUrl`, the one canonical PR-URL seam (GitHub `/pull/` on github.com, Forgejo `/pulls/` on any lowercase DNS host) shared by prose link derivation, `--pr` validation, and public-followup `pr_url`; near-misses derive as `doc` links, never `pr`. - `src/derive.ts` - worker `blocked` / `ready` / active `held` and public delivery readiness are derived in the CLI from `list` + the dep graph + hold date gates, never Store methods, so every backend gets them for free. - `src/backends/markdown*.ts` — the only P1 backend. - `src/public-followup.ts` - authoritative versioned schema, strict privacy-safe validation, canonical encoding, immutable-field checks, relation/event readiness, and terminal-state invariants for `kind=public-followup`; `src/commands/public-followup.ts` owns its dedicated CLI state machine. diff --git a/README.md b/README.md index 36c4ccc..a9221b3 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ tasks-axi add lavish-foo-q9 "fix summary toggle" --kind ship --repo lavish-axi - # move through the workflow tasks-axi start firstmate-lease-adopt tasks-axi done sm-idle-handoff-q8 --pr https://github.com/owner/repo/pull/42 +tasks-axi done fj-task-q1 --pr https://forgejo.example.com/owner/repo/pulls/39 tasks-axi reopen some-task # dependencies, holds, and the ready queue diff --git a/src/backends/markdown-grammar.ts b/src/backends/markdown-grammar.ts index 22ec5d9..ef82a01 100644 --- a/src/backends/markdown-grammar.ts +++ b/src/backends/markdown-grammar.ts @@ -1,6 +1,7 @@ import { AxiError } from "../errors.js"; import type { Dep, Hold, HoldKind, State, Task, TaskLink } from "../model.js"; import { HOLD_KINDS } from "../model.js"; +import { isPrUrl } from "../pr-url.js"; import { PUBLIC_FOLLOWUP_KIND, assertPublicFollowupTaskState, @@ -125,7 +126,6 @@ const TAIL_HOLD_KIND = new RegExp( ); const TAIL_HOLD_UNTIL = new RegExp(`\\s*\\(hold-until:\\s*(${DATE})\\)\\s*$`); -const PR_LINK = /https?:\/\/\S+?\/pull\/\d+/g; const REPORT_LINK = /\bdata\/\S+?\/report\.md\b/g; const GENERIC_URL = /https?:\/\/\S+/g; @@ -162,10 +162,12 @@ export function deriveLinks(text: string): TaskLink[] { seen.add(url); links.push({ kind, url }); }; - for (const m of text.matchAll(PR_LINK)) add("pr", m[0]); + for (const m of text.matchAll(GENERIC_URL)) { + if (isPrUrl(trimUrl(m[0]))) add("pr", m[0]); + } for (const m of text.matchAll(REPORT_LINK)) add("report", m[0]); for (const m of text.matchAll(GENERIC_URL)) { - if (!/\/pull\/\d+/.test(m[0])) add("doc", m[0]); + if (!isPrUrl(trimUrl(m[0]))) add("doc", m[0]); } return links; } diff --git a/src/backends/markdown.ts b/src/backends/markdown.ts index aa9e612..cd1adfc 100644 --- a/src/backends/markdown.ts +++ b/src/backends/markdown.ts @@ -22,6 +22,7 @@ import type { TransitionOpts, } from "../model.js"; import { HOLD_KINDS } from "../model.js"; +import { PR_URL_EXPECTED } from "../pr-url.js"; import { PUBLIC_FOLLOWUP_KIND, assertPublicFollowupMutation, @@ -140,7 +141,9 @@ function normalizeLinkUrl(url: string): string { } function normalizeTypedLink(link: TaskLink): TaskLink { - const url = normalizeLinkUrl(link.url); + const normalized = normalizeLinkUrl(link.url); + // pr URLs must already be canonical; padded input is rejected, not trimmed + const url = link.kind === "pr" ? link.url : normalized; const derived = deriveLinks(url); if ( !derived.some( @@ -149,7 +152,7 @@ function normalizeTypedLink(link: TaskLink): TaskLink { ) { const expected = link.kind === "pr" - ? "an http(s) pull request URL ending in /pull/" + ? PR_URL_EXPECTED : link.kind === "report" ? "a data//report.md path" : "an http(s) URL"; diff --git a/src/commands/crud.ts b/src/commands/crud.ts index 8bcb36f..d075976 100644 --- a/src/commands/crud.ts +++ b/src/commands/crud.ts @@ -10,6 +10,7 @@ import { } from "../args.js"; import { takeBody } from "../body.js"; import { deriveLinks, extractTags } from "../backends/markdown-grammar.js"; +import { PR_URL_EXPECTED } from "../pr-url.js"; import { renderMutation, stateLabel, taskToJson } from "../confirm.js"; import { requireCtx, type TasksContext } from "../context.js"; import { blockedIds, heldTasks } from "../derive.js"; @@ -140,14 +141,13 @@ function requireTypedLinkUrl( `Pass ${flag}=... without line breaks`, ]); } - const url = checked.trim(); + // pr URLs must already be canonical; padded input is rejected, not trimmed + const url = kind === "pr" ? checked : checked.trim(); if ( !deriveLinks(url).some((link) => link.kind === kind && link.url === url) ) { const expected = - kind === "pr" - ? "an http(s) pull request URL ending in /pull/" - : "a data//report.md path"; + kind === "pr" ? PR_URL_EXPECTED : "a data//report.md path"; throw new AxiError(`${flag} must be ${expected}`, "VALIDATION_ERROR"); } return url; diff --git a/src/commands/state.ts b/src/commands/state.ts index 5126f22..d3ab086 100644 --- a/src/commands/state.ts +++ b/src/commands/state.ts @@ -57,6 +57,7 @@ flags: --json print the resulting task as a JSON object examples: tasks-axi done sm-idle-handoff-q8 --pr https://github.com/o/r/pull/42 + tasks-axi done fj-task-q1 --pr https://forgejo.example.com/o/r/pulls/39 tasks-axi done pr31-review-r6 --report data/pr31-review-r6/report.md`; export const REOPEN_HELP = `usage: tasks-axi reopen diff --git a/src/pr-url.ts b/src/pr-url.ts new file mode 100644 index 0000000..ac68948 --- /dev/null +++ b/src/pr-url.ts @@ -0,0 +1,32 @@ +/** + * Canonical pull request URL classification - the single seam shared by prose + * link derivation (deriveLinks), typed-link validation (`done --pr`, `add --pr`, + * backend normalization), and public-followup `pr_url` deliverables. + * + * Exactly two byte-for-byte shapes are PR URLs: + * - GitHub: https://github.com///pull/ + * - Forgejo: https://///pulls/ + * with a positive number without leading zeros. Anything else - issue URLs, + * singular/plural route confusion, trailing slash, query/fragment, whitespace, + * userinfo, ports, encoded separators - is not a PR URL. + */ + +const HOST_LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?"; +const SEGMENT = "[A-Za-z0-9._-]+"; +const PR_URL_RE = new RegExp( + `^https://(${HOST_LABEL}(?:\\.${HOST_LABEL})*)/(${SEGMENT})/(${SEGMENT})/(pull|pulls)/([1-9][0-9]*)$`, +); + +export const PR_URL_EXPECTED = + "a canonical pull request URL: https://github.com///pull/ (GitHub) or https://///pulls/ (Forgejo)"; + +/** True when url is byte-for-byte a canonical GitHub or Forgejo PR URL. */ +export function isPrUrl(url: string): boolean { + const m = PR_URL_RE.exec(url); + if (m === null) return false; + const [, host, owner, repo, route] = m; + if (owner === "." || owner === ".." || repo === "." || repo === "..") { + return false; + } + return route === "pull" ? host === "github.com" : host !== "github.com"; +} diff --git a/src/public-followup.ts b/src/public-followup.ts index ca205d8..cbd3b3a 100644 --- a/src/public-followup.ts +++ b/src/public-followup.ts @@ -1,4 +1,5 @@ import { AxiError } from "./errors.js"; +import { isPrUrl } from "./pr-url.js"; export const PUBLIC_FOLLOWUP_KIND = "public-followup"; export const PUBLIC_FOLLOWUP_SCHEMA_VERSION = 1 as const; @@ -209,7 +210,6 @@ const SHA256_RE = /^[a-f0-9]{64}$/; const DELIVERABLE_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/; const SAFE_CODE_RE = /^[a-z][a-z0-9._-]{0,63}$/; const PROJECT_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,79}$/; -const PR_URL_RE = /^https:\/\/[^?#\s]+\/pull\/\d+$/; const REPORT_PATH_RE = /^data\/[A-Za-z0-9][A-Za-z0-9._-]*\/report\.md$/; const COMMIT_SHA_RE = /^[a-f0-9]{7,64}$/; const MAX_ENCODED_METADATA_LENGTH = 1_000_000; @@ -1367,15 +1367,7 @@ function deliverablesAreSafeForExpected( return false; } for (const [name, value] of Object.entries(deliverables)) { - if (name === "pr_url") { - if (!PR_URL_RE.test(value)) return false; - try { - const url = new URL(value); - if (url.username !== "" || url.password !== "") return false; - } catch { - return false; - } - } + if (name === "pr_url" && !isPrUrl(value)) return false; if (name === "report_path" && !REPORT_PATH_RE.test(value)) return false; if (name === "commit_sha" && !COMMIT_SHA_RE.test(value)) return false; if (name === "error_code" && !SAFE_CODE_RE.test(value)) return false; diff --git a/test/backends/markdown-grammar.test.ts b/test/backends/markdown-grammar.test.ts index 7fe8471..5989fe3 100644 --- a/test/backends/markdown-grammar.test.ts +++ b/test/backends/markdown-grammar.test.ts @@ -326,6 +326,42 @@ describe("markdown grammar", () => { }); expect(links).toContainEqual({ kind: "report", url: "data/x/report.md" }); }); + + it("derives a Forgejo pulls URL as the single pr link", () => { + const links = deriveLinks( + "merged https://forgejo.samesies.gay/eve/orchalycious/pulls/39", + ); + expect(links).toEqual([ + { + kind: "pr", + url: "https://forgejo.samesies.gay/eve/orchalycious/pulls/39", + }, + ]); + }); + + it("keeps non-canonical PR-ish URLs as doc links, never pr", () => { + for (const url of [ + "https://forgejo.samesies.gay/eve/orchalycious/pull/39", + "https://github.com/o/r/pulls/42", + "https://forgejo.samesies.gay/o/r/pulls/39?tab=files", + ]) { + expect(deriveLinks(`see ${url}`)).toEqual([{ kind: "doc", url }]); + } + }); + + it("round-trips a done bullet with a Forgejo pull URL byte-exactly", () => { + const src = + "## Queued\n\n## Done\n- [x] fj-done-q1 - merged https://forgejo.samesies.gay/eve/orchalycious/pulls/39 (merged 2026-08-07)\n"; + const doc = parseBacklog(src); + const task = tasksOf(doc)[0]; + expect(task.links).toEqual([ + { + kind: "pr", + url: "https://forgejo.samesies.gay/eve/orchalycious/pulls/39", + }, + ]); + expect(renderBacklog(doc)).toBe(src); + }); }); describe("canonical render", () => { diff --git a/test/backends/markdown.test.ts b/test/backends/markdown.test.ts index cfd06c6..3ff1b3a 100644 --- a/test/backends/markdown.test.ts +++ b/test/backends/markdown.test.ts @@ -530,6 +530,11 @@ describe("MarkdownStore", () => { addLinks: [{ kind: "pr", url: "https://github.com/o/r/issues/9" }], }), ).rejects.toMatchObject({ code: "VALIDATION_ERROR" }); + await expect( + b.store.update("cert-cleanup", { + addLinks: [{ kind: "pr", url: " https://github.com/o/r/pull/9 " }], + }), + ).rejects.toMatchObject({ code: "VALIDATION_ERROR" }); await expect( b.store.update("cert-cleanup", { addLinks: [{ kind: "report", url: "reports/cert/report.md" }], diff --git a/test/commands/public-followup.test.ts b/test/commands/public-followup.test.ts index f47a5d0..075fddb 100644 --- a/test/commands/public-followup.test.ts +++ b/test/commands/public-followup.test.ts @@ -361,6 +361,48 @@ describe("public-followup commands", () => { } }); + it("accepts a Forgejo pulls URL as a pr-merged deliverable", async () => { + const b = makeBacklog(EMPTY); + try { + await add(b); + await bind(b); + const landed = await acceptEvent( + b, + event("evt-forgejo-url", "rel-code", "work-code-q1", 1, { + deliverables: { + pr_url: "https://forgejo.samesies.gay/eve/orchalycious/pulls/39", + }, + }), + ); + expect( + landed.task.public_followup.work_relations[0].accepted_events[0] + .deliverables.pr_url, + ).toBe("https://forgejo.samesies.gay/eve/orchalycious/pulls/39"); + } finally { + b.cleanup(); + } + }); + + it("rejects a singular-route Forgejo PR URL before accepting work", async () => { + const b = makeBacklog(EMPTY); + try { + await add(b); + await bind(b); + await expect( + acceptEvent( + b, + event("evt-singular-url", "rel-code", "work-code-q1", 1, { + deliverables: { + pr_url: "https://forgejo.samesies.gay/eve/orchalycious/pull/39", + }, + }), + ), + ).rejects.toMatchObject({ code: "VALIDATION_ERROR" }); + } finally { + b.cleanup(); + } + }); + it("keeps a failed required relation actionable unless failure is expected", async () => { const b = makeBacklog(EMPTY); try { diff --git a/test/commands/state.test.ts b/test/commands/state.test.ts index 7744c5c..f311fff 100644 --- a/test/commands/state.test.ts +++ b/test/commands/state.test.ts @@ -125,6 +125,50 @@ describe("state commands", () => { } }); + it("closes with a Forgejo pulls URL and preserves it byte-for-byte", async () => { + const b = makeBacklog(); + try { + const out = await doneCommand( + [ + "cert-cleanup", + "--pr", + "https://forgejo.samesies.gay/eve/orchalycious/pulls/39", + "--no-prune", + ], + b.ctx, + ); + expect(out).toContain( + "done cert-cleanup -> Done (pr https://forgejo.samesies.gay/eve/orchalycious/pulls/39)", + ); + const read = b.read(); + expect(read).toContain( + "https://forgejo.samesies.gay/eve/orchalycious/pulls/39", + ); + expect(read).toContain("(merged 2026-07-01)"); + } finally { + b.cleanup(); + } + }); + + it("rejects non-canonical pull URLs without mutating", async () => { + const b = makeBacklog(); + try { + for (const url of [ + "https://forgejo.samesies.gay/eve/orchalycious/pull/39", + "https://github.com/o/r/pulls/9", + "https://github.com/o/r/pull/9?w=1", + " https://github.com/o/r/pull/9 ", + ]) { + await expect( + doneCommand(["cert-cleanup", "--pr", url, "--no-prune"], b.ctx), + ).rejects.toMatchObject({ code: "VALIDATION_ERROR" }); + } + expect(b.read()).toContain("- [ ] cert-cleanup"); + } finally { + b.cleanup(); + } + }); + it("emits a machine-readable task and pruned count with --json", async () => { const b = makeBacklog(); try { diff --git a/test/pr-url.test.ts b/test/pr-url.test.ts new file mode 100644 index 0000000..1a3b21c --- /dev/null +++ b/test/pr-url.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; + +import { isPrUrl } from "../src/pr-url.js"; + +describe("isPrUrl", () => { + it.each([ + "https://github.com/o/r/pull/42", + "https://github.com/some-owner/some.repo/pull/1", + "https://forgejo.samesies.gay/eve/orchalycious/pulls/39", + "https://codeberg.org/forgejo/forgejo/pulls/1234", + ])("accepts canonical PR URL %s", (url) => { + expect(isPrUrl(url)).toBe(true); + }); + + it.each([ + // singular/plural route confusion + "https://github.com/o/r/pulls/42", + "https://forgejo.samesies.gay/eve/orchalycious/pull/39", + // issue URLs + "https://github.com/o/r/issues/42", + "https://forgejo.samesies.gay/o/r/issues/42", + // number shape + "https://github.com/o/r/pull/0", + "https://github.com/o/r/pull/042", + "https://github.com/o/r/pull/42abc", + "https://forgejo.samesies.gay/o/r/pulls/0", + // scheme / decoration + "http://github.com/o/r/pull/42", + "https://github.com/o/r/pull/42/", + "https://github.com/o/r/pull/42?w=1", + "https://github.com/o/r/pull/42#top", + " https://github.com/o/r/pull/42", + "https://github.com/o/r/pull/42\n", + "https://github.com/o/r/pull/4\u00002", + "https://github.com/o/r/pull/4 2", + // authority shape + "https://user@forgejo.samesies.gay/o/r/pulls/39", + "https://token:secret@github.com/o/r/pull/519", + "https://forgejo.samesies.gay:8443/o/r/pulls/39", + "https://Forgejo.Samesies.Gay/o/r/pulls/39", + "https://-bad-.example.com/o/r/pulls/39", + // path shape + "https://forgejo.samesies.gay/o/r/extra/pulls/39", + "https://forgejo.samesies.gay/r/pulls/39", + "https://forgejo.samesies.gay//r/pulls/39", + "https://forgejo.samesies.gay/o%2Fx/r/pulls/39", + "https://forgejo.samesies.gay/../r/pulls/39", + "https://forgejo.samesies.gay/o/../pulls/39", + ])("rejects non-canonical URL %j", (url) => { + expect(isPrUrl(url)).toBe(false); + }); +}); From 8acfbebaf263a13a177c4f404f0cdcd2098e730c Mon Sep 17 00:00:00 2001 From: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:00:46 -0700 Subject: [PATCH 04/10] chore: remove committed no-mistakes evidence (now on orphan branch) (#39) --- .../workflow-acceptance.txt | 62 - .../workflow_acceptance.rb | 126 -- .../cli-mutation-transcript.txt | 203 ---- .../fm/tasks-axi-confirm-w5/done-archive.md | 3 - .../fm/tasks-axi-confirm-w5/e2e-backlog.md | 12 - .../tasks-axi-confirm-w5/e2e-destination.md | 8 - .../tasks-axi-drop-append/cli-transcript.txt | 97 -- .../fm/tasks-axi-drop-append/e2e/backlog.md | 9 - .../tasks-axi-drop-append/e2e/note-archive.md | 5 - .../hold-state-e2e-transcript.txt | 189 --- .../hold-state-e2e.backlog.md | 15 - .../fm/tasks-axi-p1-b8/cli-e2e-transcript.md | 139 --- .../fm/tasks-axi-p1-b8/cli-e2e/backlog.md | 19 - .../tasks-axi-p1-b8/cli-e2e/done-archive.md | 8 - .../destination-backlog.md | 8 - .../destination-template.md | 8 - .../tasks-public-followup-k4/done-archive.md | 4 - .../e2e-transcript.log | 1043 ----------------- .../fm/tasks-public-followup-k4/e2e.sh | 98 -- .../fm/tasks-public-followup-k4/event.json | 16 - .../fm/tasks-public-followup-k4/expected.json | 8 - .../fm/tasks-public-followup-k4/receipt.json | 11 - .../fm/tasks-public-followup-k4/relation.json | 10 - .../fm/tasks-public-followup-k4/request.json | 12 - .../source-backlog.md | 6 - .../source-template.md | 7 - .../cli-transcript.txt | 29 - .../version-module-trace.txt | 3 - .../fm/tax-release-r7/bin-shebang.txt | 2 - .../excluded-dev-artifacts-check.txt | 2 - .../install-from-tarball-smoke.txt | 14 - .../fm/tax-release-r7/npm-pack-json.txt | 170 --- .../fm/tax-release-r7/npm-publish-dry-run.txt | 53 - .../tax-release-r7/packed-package-json.json | 65 - .../fm/tax-release-r7/tarball-file-list.txt | 30 - .../fm/tax-release-r7/tasks-axi-0.1.0.tgz | Bin 35740 -> 0 bytes .../cli-done-readback.toon | 18 - .../fm/taxi-body-blanks-b5/cli-move-source.md | 10 - .../fm/taxi-body-blanks-b5/cli-move-target.md | 16 - .../taxi-body-blanks-b5/cli-mv-readback.toon | 18 - .../cli-start-readback.toon | 18 - .../fm/taxi-body-blanks-b5/cli-workflow.md | 25 - .../fm/taxi-docs-d8/npm-pack-dry-run.txt | 51 - .../fm/taxi-docs-d8/update-body-file-notes.md | 2 - .../fm/taxi-docs-d8/update-e2e-backlog.md | 9 - .../fm/taxi-docs-d8/update-e2e-transcript.txt | 166 --- .../fm/taxi-fmt-f9/firstmate-cli-backlog.md | 10 - .../taxi-fmt-f9/firstmate-cli-transcript.txt | 87 -- .../taxi-mv-linked-sets-m3/cli-transcript.md | 34 - .../refusal-destination-after.md | 7 - .../refusal-source-after.md | 10 - .../success-destination-after.md | 19 - .../success-source-after.md | 7 - 53 files changed, 3001 deletions(-) delete mode 100644 .no-mistakes/evidence/fm/nm-body-events-tasks-axi-r1/workflow-acceptance.txt delete mode 100644 .no-mistakes/evidence/fm/nm-body-events-tasks-axi-r1/workflow_acceptance.rb delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-confirm-w5/cli-mutation-transcript.txt delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-confirm-w5/done-archive.md delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-destination.md delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-drop-append/cli-transcript.txt delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/backlog.md delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/note-archive.md delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e-transcript.txt delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e-transcript.md delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md delete mode 100644 .no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/done-archive.md delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/destination-template.md delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/done-archive.md delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/e2e-transcript.log delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/e2e.sh delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/event.json delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/expected.json delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/receipt.json delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/relation.json delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/request.json delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/source-backlog.md delete mode 100644 .no-mistakes/evidence/fm/tasks-public-followup-k4/source-template.md delete mode 100644 .no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/cli-transcript.txt delete mode 100644 .no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/version-module-trace.txt delete mode 100644 .no-mistakes/evidence/fm/tax-release-r7/bin-shebang.txt delete mode 100644 .no-mistakes/evidence/fm/tax-release-r7/excluded-dev-artifacts-check.txt delete mode 100644 .no-mistakes/evidence/fm/tax-release-r7/install-from-tarball-smoke.txt delete mode 100644 .no-mistakes/evidence/fm/tax-release-r7/npm-pack-json.txt delete mode 100644 .no-mistakes/evidence/fm/tax-release-r7/npm-publish-dry-run.txt delete mode 100644 .no-mistakes/evidence/fm/tax-release-r7/packed-package-json.json delete mode 100644 .no-mistakes/evidence/fm/tax-release-r7/tarball-file-list.txt delete mode 100644 .no-mistakes/evidence/fm/tax-release-r7/tasks-axi-0.1.0.tgz delete mode 100644 .no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-done-readback.toon delete mode 100644 .no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-move-source.md delete mode 100644 .no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-move-target.md delete mode 100644 .no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-mv-readback.toon delete mode 100644 .no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-start-readback.toon delete mode 100644 .no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-workflow.md delete mode 100644 .no-mistakes/evidence/fm/taxi-docs-d8/npm-pack-dry-run.txt delete mode 100644 .no-mistakes/evidence/fm/taxi-docs-d8/update-body-file-notes.md delete mode 100644 .no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md delete mode 100644 .no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-transcript.txt delete mode 100644 .no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md delete mode 100644 .no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-transcript.txt delete mode 100644 .no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/cli-transcript.md delete mode 100644 .no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/refusal-destination-after.md delete mode 100644 .no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/refusal-source-after.md delete mode 100644 .no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-destination-after.md delete mode 100644 .no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-source-after.md diff --git a/.no-mistakes/evidence/fm/nm-body-events-tasks-axi-r1/workflow-acceptance.txt b/.no-mistakes/evidence/fm/nm-body-events-tasks-axi-r1/workflow-acceptance.txt deleted file mode 100644 index ff61a83..0000000 --- a/.no-mistakes/evidence/fm/nm-body-events-tasks-axi-r1/workflow-acceptance.txt +++ /dev/null @@ -1,62 +0,0 @@ -tasks-axi no-mistakes workflow acceptance - -PASS: committed scope is only the required workflow -PASS: the workflow change contains exactly two hunks -PASS: YAML syntax parses -PASS: canonical run-name is exact -PASS: opened/edited use run_id while head changes coalesce -PASS: cancel-in-progress remains true -PASS: pull_request trigger and event set are preserved -PASS: main branch filter is preserved -PASS: permissions remain contents read-only -PASS: workflow does not use pull_request_target -PASS: workflow does not reference secrets -PASS: workflow does not check out or execute fork code -PASS: stable check name is preserved -PASS: github-actions[bot] exemption is preserved -PASS: dependabot[bot] exemption is preserved -PASS: release-please[bot] exemption is preserved -PASS: signature marker is preserved exactly - -Resolved run identities -unsigned opened: - group: no-mistakes-required-42-9001 - run-name: PR #42 body compliance - opened - event 301 (run 9001) -signed edited: - group: no-mistakes-required-42-9002 - run-name: PR #42 body compliance - edited - event 302 (run 9002) -signed same-head replay: - group: no-mistakes-required-42-9003 - run-name: PR #42 body compliance - edited - event 303 (run 9003) -synchronize: - group: no-mistakes-required-42-head-change - run-name: PR #42 body compliance - synchronize - event 304 (run 9004) -reopened: - group: no-mistakes-required-42-head-change - run-name: PR #42 body compliance - reopened - event 305 (run 9005) -PASS: opened, edited, and same-head replay cannot collapse -PASS: synchronize and reopened retain head-change coalescing - -unsigned opened body: exit 1 -::error::This PR was not raised through no-mistakes. - -Contributions to this repository must be submitted via 'git push no-mistakes'. -That pipeline runs the required review/test/lint/CI steps and writes a -deterministic '## Pipeline' section into the PR body containing: - - Updates from [git push no-mistakes](https://github.com/kunchenguid/no-mistakes) - -See CONTRIBUTING.md for setup and the full workflow. - -PR author: contributor - -signed edited body: exit 0 -Found no-mistakes signature in PR #42 body. - -signed same-head edited replay: exit 0 -Found no-mistakes signature in PR #42 body. -PASS: unsigned body is rejected -PASS: signed edit is accepted -PASS: signed same-head replay is accepted - -RESULT: workflow acceptance passed diff --git a/.no-mistakes/evidence/fm/nm-body-events-tasks-axi-r1/workflow_acceptance.rb b/.no-mistakes/evidence/fm/nm-body-events-tasks-axi-r1/workflow_acceptance.rb deleted file mode 100644 index d0bb07c..0000000 --- a/.no-mistakes/evidence/fm/nm-body-events-tasks-axi-r1/workflow_acceptance.rb +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env ruby - -require "open3" -require "yaml" - -workflow_path = ".github/workflows/no-mistakes-required.yml" -base = "0e2cf5904cc227bb66fed66bddac23da6ee449db" -target = "460bdb68cbf7e0cb5692bb2f267676e53ed6c9f8" -source = File.read(workflow_path) -workflow = YAML.load_file(workflow_path) - -def assert(label, condition) - raise "FAIL: #{label}" unless condition - - puts "PASS: #{label}" -end - -puts "tasks-axi no-mistakes workflow acceptance" -puts - -changed_files, changed_status = Open3.capture2( - "git", "diff", "--name-only", "#{base}..#{target}" -) -assert("committed scope is only the required workflow", changed_status.success? && - changed_files.lines.map(&:strip) == [workflow_path]) - -diff, diff_status = Open3.capture2( - "git", "diff", "#{base}..#{target}", "--", workflow_path -) -assert("the workflow change contains exactly two hunks", - diff_status.success? && diff.lines.count { |line| line.start_with?("@@") } == 2) - -expected_run_name = 'run-name: "PR #${{ github.event.pull_request.number }} body compliance - ${{ github.event.action }} - event ${{ github.run_number }} (run ${{ github.run_id }})"' -expected_group = "group: no-mistakes-required-${{ github.event.pull_request.number }}-${{ (github.event.action == 'opened' || github.event.action == 'edited') && github.run_id || 'head-change' }}" - -assert("YAML syntax parses", workflow.is_a?(Hash)) -assert("canonical run-name is exact", source.include?(expected_run_name)) -assert("opened/edited use run_id while head changes coalesce", source.include?(expected_group)) -assert("cancel-in-progress remains true", source.match?(/^\s+cancel-in-progress: true$/)) - -assert("pull_request trigger and event set are preserved", - source.include?("pull_request:\n types: [opened, edited, synchronize, reopened]")) -assert("main branch filter is preserved", source.include?("branches:\n - main")) -assert("permissions remain contents read-only", - source.include?("permissions:\n contents: read\n") && - !source.match?(/pull-requests:\s*write/) && - !source.match?(/contents:\s*write/)) -assert("workflow does not use pull_request_target", !source.include?("pull_request_target")) -assert("workflow does not reference secrets", !source.match?(/\bsecrets\./)) -assert("workflow does not check out or execute fork code", - !source.include?("actions/checkout") && !source.match?(/\bgithub\.event\.pull_request\.head\./)) -assert("stable check name is preserved", source.include?("name: PR must be raised via no-mistakes")) - -%w[github-actions[bot] dependabot[bot] release-please[bot]].each do |bot| - assert("#{bot} exemption is preserved", source.include?("'#{bot}'")) -end - -marker = "Updates from [git push no-mistakes](https://github.com/kunchenguid/no-mistakes)" -assert("signature marker is preserved exactly", source.include?("marker='#{marker}'")) - -def identity(pr:, action:, run_number:, run_id:) - suffix = %w[opened edited].include?(action) ? run_id : "head-change" - group = "no-mistakes-required-#{pr}-#{suffix}" - name = "PR ##{pr} body compliance - #{action} - event #{run_number} (run #{run_id})" - [group, name] -end - -events = [ - ["unsigned opened", 42, "opened", 301, 9001], - ["signed edited", 42, "edited", 302, 9002], - ["signed same-head replay", 42, "edited", 303, 9003], - ["synchronize", 42, "synchronize", 304, 9004], - ["reopened", 42, "reopened", 305, 9005] -] - -puts -puts "Resolved run identities" -events.each do |label, pr, action, run_number, run_id| - group, name = identity(pr: pr, action: action, run_number: run_number, run_id: run_id) - puts "#{label}:" - puts " group: #{group}" - puts " run-name: #{name}" -end - -opened_group = identity(pr: 42, action: "opened", run_number: 301, run_id: 9001).first -edit_group = identity(pr: 42, action: "edited", run_number: 302, run_id: 9002).first -replay_group = identity(pr: 42, action: "edited", run_number: 303, run_id: 9003).first -sync_group = identity(pr: 42, action: "synchronize", run_number: 304, run_id: 9004).first -reopen_group = identity(pr: 42, action: "reopened", run_number: 305, run_id: 9005).first - -assert("opened, edited, and same-head replay cannot collapse", - [opened_group, edit_group, replay_group].uniq.length == 3) -assert("synchronize and reopened retain head-change coalescing", sync_group == reopen_group) - -run_script = workflow.fetch("jobs").fetch("check").fetch("steps").first.fetch("run") - -def replay(run_script, label, body) - stdout, stderr, status = Open3.capture3( - { - "PR_BODY" => body, - "PR_AUTHOR" => "contributor", - "PR_NUMBER" => "42" - }, - "bash", "-c", run_script - ) - puts - puts "#{label}: exit #{status.exitstatus}" - print stdout - print stderr - status.exitstatus -end - -unsigned = replay(run_script, "unsigned opened body", "A normal pull request body.") -signed = replay(run_script, "signed edited body", "## Pipeline\n\n#{marker}") -same_head = replay( - run_script, - "signed same-head edited replay", - "Updated context, unchanged head.\n\n## Pipeline\n\n#{marker}" -) - -assert("unsigned body is rejected", unsigned == 1) -assert("signed edit is accepted", signed == 0) -assert("signed same-head replay is accepted", same_head == 0) - -puts -puts "RESULT: workflow acceptance passed" diff --git a/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/cli-mutation-transcript.txt b/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/cli-mutation-transcript.txt deleted file mode 100644 index 478b859..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/cli-mutation-transcript.txt +++ /dev/null @@ -1,203 +0,0 @@ -tasks-axi mutation-output E2E evidence -backlog: .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md - -$ pnpm --silent dev add grok-harness-g7 confirm\ mutation\ output --kind ship --repo firstmate --start --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: added grok-harness-g7 (ship, repo firstmate) -> In flight -task: - id: grok-harness-g7 - title: confirm mutation output - state: in_flight - blocked: no - blocked_by: none - kind: ship - repo: firstmate - priority: "-" - created: 2026-06-29 - closed: "-" - deps: none - links: none - body: "" -help[2]: - - Run `tasks-axi done grok-harness-g7 --pr --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` when it ships - - Run `tasks-axi block grok-harness-g7 --by --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to record a dependency - -$ pnpm --silent dev done grok-harness-g7 --pr https://github.com/kunchenguid/tasks-axi/pull/123 --no-prune --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: done grok-harness-g7 -> Done (pr https://github.com/kunchenguid/tasks-axi/pull/123) -help[1]: - - Run `tasks-axi ready --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to dispatch work unblocked by this - -$ pnpm --silent dev add queued-next-q1 queued\ follow-up --repo firstmate --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: added queued-next-q1 (repo firstmate) -> Queued -task: - id: queued-next-q1 - title: queued follow-up - state: queued - blocked: no - blocked_by: none - kind: task - repo: firstmate - priority: "-" - created: 2026-06-29 - closed: "-" - deps: none - links: none - body: "" -help[2]: - - Run `tasks-axi start queued-next-q1 --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to move it to in flight - - Run `tasks-axi block queued-next-q1 --by --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to record a dependency - -$ pnpm --silent dev start queued-next-q1 --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: start queued-next-q1 -> In flight -help[1]: - - Run `tasks-axi done queued-next-q1 --pr --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` when it ships - -$ pnpm --silent dev block queued-next-q1 --by fix-login-k3 --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: block queued-next-q1 -> blocked-by fix-login-k3 -help[2]: - - Run `tasks-axi unblock queued-next-q1 --by --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to clear it - - Run `tasks-axi ready --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to see what is still dispatchable - -$ pnpm --silent dev unblock queued-next-q1 --by fix-login-k3 --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: unblock queued-next-q1 -> cleared fix-login-k3 -help[1]: - - Run `tasks-axi ready --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to see newly unblocked work - -$ pnpm --silent dev update queued-next-q1 --title renamed\ follow-up --append Evidence\ note\ added\ through\ update. --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: updated queued-next-q1 (title, note) -task: - id: queued-next-q1 - title: renamed follow-up - state: in_flight - blocked: no - blocked_by: none - kind: task - repo: firstmate - priority: "-" - created: 2026-06-29 - closed: "-" - deps: none - links: none - body: Evidence note added through update. -help[1]: - - Run `tasks-axi show queued-next-q1 --full --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to see the result - -$ pnpm --silent dev reopen grok-harness-g7 --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: reopen grok-harness-g7 -> Queued -help[1]: - - Run `tasks-axi start grok-harness-g7 --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to move it to in flight - -$ pnpm --silent dev rm queued-next-q1 --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: removed queued-next-q1 -help[1]: - - Run `tasks-axi list --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to see remaining tasks - -$ pnpm --silent dev add move-me-q1 move\ candidate --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: added move-me-q1 -> Queued -task: - id: move-me-q1 - title: move candidate - state: queued - blocked: no - blocked_by: none - kind: task - repo: "-" - priority: "-" - created: 2026-06-29 - closed: "-" - deps: none - links: none - body: "" -help[2]: - - Run `tasks-axi start move-me-q1 --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to move it to in flight - - Run `tasks-axi block move-me-q1 --by --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to record a dependency - -$ pnpm --silent dev mv move-me-q1 --to .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-destination.md --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: mv move-me-q1 -> /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KWAQXHGJT5N3C63TF37X05YN/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-destination.md -help[1]: - - Run `tasks-axi list --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to see remaining tasks - -$ pnpm --silent dev add archive-candidate-q1 archive\ candidate --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: added archive-candidate-q1 -> Queued -task: - id: archive-candidate-q1 - title: archive candidate - state: queued - blocked: no - blocked_by: none - kind: task - repo: "-" - priority: "-" - created: 2026-06-29 - closed: "-" - deps: none - links: none - body: "" -help[2]: - - Run `tasks-axi start archive-candidate-q1 --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to move it to in flight - - Run `tasks-axi block archive-candidate-q1 --by --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to record a dependency - -$ pnpm --silent dev done archive-candidate-q1 --no-prune --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: done archive-candidate-q1 -> Done -help[1]: - - Run `tasks-axi ready --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to dispatch work unblocked by this - -$ pnpm --silent dev prune --keep 1 --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: prune done -> archived 1 (kept 1) -help[1]: - - Run `tasks-axi list --state done --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to see retained Done items - -$ pnpm --silent dev render --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -ok: render -> normalized 4 -help[1]: - - Run `tasks-axi list --file=.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md` to see the normalized backlog - -$ pnpm --silent dev add json-q1 machine\ readable\ confirmation --repo firstmate --json --file .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -{ - "ok": true, - "action": "add", - "task": { - "id": "json-q1", - "title": "machine readable confirmation", - "state": "queued", - "kind": null, - "repo": "firstmate", - "priority": null, - "created": "2026-06-29", - "closed": null, - "deps": [], - "links": [], - "body": null, - "blocked": false, - "blocked_by": [] - } -} - -$ sed -n 1\,220p .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md -# Backlog - -## In flight -- [ ] fix-login-k3 - one line (repo: app, since 2026-06-20) - -## Queued -- [ ] add-tests-q7 - one line (repo: app) blocked-by: fix-login-k3 - waits on the login refactor -- [ ] grok-harness-g7 - confirm mutation output https://github.com/kunchenguid/tasks-axi/pull/123 (repo: firstmate) (kind: ship) -- [ ] json-q1 - machine readable confirmation (repo: firstmate) (since 2026-06-29) - -## Done -- [x] archive-candidate-q1 - archive candidate (done 2026-06-29) - -$ sed -n 1\,220p .no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-destination.md -# Backlog - -## In flight - -## Queued -- [ ] move-me-q1 - move candidate (since 2026-06-29) - -## Done - -$ sed -n 1\,220p .no-mistakes/evidence/fm/tasks-axi-confirm-w5/done-archive.md - -## Archived 2026-06-29 -- [x] legacy-done-z1 - one line - https://github.com/o/r/pull/12 (merged 2026-06-21) - diff --git a/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/done-archive.md b/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/done-archive.md deleted file mode 100644 index 86cc9d4..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/done-archive.md +++ /dev/null @@ -1,3 +0,0 @@ - -## Archived 2026-06-29 -- [x] legacy-done-z1 - one line - https://github.com/o/r/pull/12 (merged 2026-06-21) diff --git a/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md b/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md deleted file mode 100644 index ac72b92..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-backlog.md +++ /dev/null @@ -1,12 +0,0 @@ -# Backlog - -## In flight -- [ ] fix-login-k3 - one line (repo: app, since 2026-06-20) - -## Queued -- [ ] add-tests-q7 - one line (repo: app) blocked-by: fix-login-k3 - waits on the login refactor -- [ ] grok-harness-g7 - confirm mutation output https://github.com/kunchenguid/tasks-axi/pull/123 (repo: firstmate) (kind: ship) -- [ ] json-q1 - machine readable confirmation (repo: firstmate) (since 2026-06-29) - -## Done -- [x] archive-candidate-q1 - archive candidate (done 2026-06-29) diff --git a/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-destination.md b/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-destination.md deleted file mode 100644 index cdb4460..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-confirm-w5/e2e-destination.md +++ /dev/null @@ -1,8 +0,0 @@ -# Backlog - -## In flight - -## Queued -- [ ] move-me-q1 - move candidate (since 2026-06-29) - -## Done diff --git a/.no-mistakes/evidence/fm/tasks-axi-drop-append/cli-transcript.txt b/.no-mistakes/evidence/fm/tasks-axi-drop-append/cli-transcript.txt deleted file mode 100644 index 1949905..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-drop-append/cli-transcript.txt +++ /dev/null @@ -1,97 +0,0 @@ -$ pnpm exec tsx bin/tasks-axi.ts show inspect-update-q1 --full --file .no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/backlog.md -task: - id: inspect-update-q1 - title: Inspect and replace notes - state: queued - blocked: no - blocked_by: none - held: no - hold_reason: "-" - hold_kind: "-" - hold_until: "-" - kind: task - repo: "-" - priority: "-" - created: "-" - closed: "-" - deps: none - links: none - body: "old line one\nold line two" -exit=0 - -$ pnpm exec tsx bin/tasks-axi.ts update inspect-update-q1 --append step 2 should not append --file .no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/backlog.md -error: "Unknown flag: --append" -code: VALIDATION_ERROR -help[1]: Run the command with --help to see supported flags -exit=2 - -$ pnpm exec tsx bin/tasks-axi.ts update inspect-update-q1 --body new current body --archive-body --file .no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/backlog.md -ok: updated inspect-update-q1 (body, archive) -task: - id: inspect-update-q1 - title: Inspect and replace notes - state: queued - blocked: no - blocked_by: none - held: no - hold_reason: "-" - hold_kind: "-" - hold_until: "-" - kind: task - repo: "-" - priority: "-" - created: "-" - closed: "-" - deps: none - links: none - body: new current body -help[1]: - - Run `tasks-axi show inspect-update-q1 --full --file=.no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/backlog.md` to see the result -exit=0 - -$ pnpm exec tsx bin/tasks-axi.ts update inspect-update-q1 --body new current body --archive-body --json --file .no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/backlog.md -{ - "ok": true, - "action": "update", - "already": true, - "changed": [], - "task": { - "id": "inspect-update-q1", - "title": "Inspect and replace notes", - "state": "queued", - "kind": null, - "repo": null, - "priority": null, - "created": null, - "closed": null, - "deps": [], - "hold": null, - "links": [], - "body": "new current body", - "blocked": false, - "blocked_by": [], - "held": false - } -} -exit=0 - -$ sed -n 1,120p .no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/backlog.md -# Backlog - -## In flight - -## Queued -- [ ] inspect-update-q1 - Inspect and replace notes - new current body - -## Done -exit=0 - -$ sed -n 1,120p .no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/note-archive.md - -## Archived 2026-07-08 -- [ ] inspect-update-q1 - Inspect and replace notes - old line one - old line two -exit=0 - diff --git a/.no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/backlog.md b/.no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/backlog.md deleted file mode 100644 index d6d175d..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/backlog.md +++ /dev/null @@ -1,9 +0,0 @@ -# Backlog - -## In flight - -## Queued -- [ ] inspect-update-q1 - Inspect and replace notes - new current body - -## Done diff --git a/.no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/note-archive.md b/.no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/note-archive.md deleted file mode 100644 index f6cc434..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-drop-append/e2e/note-archive.md +++ /dev/null @@ -1,5 +0,0 @@ - -## Archived 2026-07-08 -- [ ] inspect-update-q1 - Inspect and replace notes - old line one - old line two diff --git a/.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e-transcript.txt b/.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e-transcript.txt deleted file mode 100644 index d88180f..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e-transcript.txt +++ /dev/null @@ -1,189 +0,0 @@ -# tasks-axi structured hold E2E transcript - -All commands run against `.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` from the worktree using the CLI entrypoint. - -$ pnpm exec tsx bin/tasks-axi.ts hold held-q1 --reason "captain decision pending" --kind captain --until 2999-01-01 --file .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -ok: hold held-q1 -> held (captain, until 2999-01-01) -task: - id: held-q1 - title: paused task - state: queued - blocked: no - blocked_by: none - held: yes - hold_reason: captain decision pending - hold_kind: captain - hold_until: 2999-01-01 - kind: task - repo: "-" - priority: "-" - created: "-" - closed: "-" - deps: none - links: none - body: "" -help[2]: - - Run `tasks-axi unhold held-q1 --file=.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` to resume dispatch - - Run `tasks-axi ready --include-held --file=.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` to review paused work - -$ pnpm exec tsx bin/tasks-axi.ts hold held-q1 --reason "captain decision pending" --kind captain --until 2999-01-01 --json --file .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -{ - "ok": true, - "action": "hold", - "already": true, - "task": { - "id": "held-q1", - "title": "paused task", - "state": "queued", - "kind": null, - "repo": null, - "priority": null, - "created": null, - "closed": null, - "deps": [], - "hold": { - "reason": "captain decision pending", - "kind": "captain", - "until": "2999-01-01" - }, - "links": [], - "body": null, - "blocked": false, - "blocked_by": [], - "held": true - } -} - -$ pnpm exec tsx bin/tasks-axi.ts hold future-q1 --reason "start after launch" --kind future --until 2999-01-01 --file .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -ok: hold future-q1 -> held (future, until 2999-01-01) -task: - id: future-q1 - title: future gated task - state: queued - blocked: no - blocked_by: none - held: yes - hold_reason: start after launch - hold_kind: future - hold_until: 2999-01-01 - kind: task - repo: "-" - priority: "-" - created: "-" - closed: "-" - deps: none - links: none - body: "" -help[2]: - - Run `tasks-axi unhold future-q1 --file=.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` to resume dispatch - - Run `tasks-axi ready --include-held --file=.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` to review paused work - -$ pnpm exec tsx bin/tasks-axi.ts hold missing-q1 --reason "wait" --file .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -error: "Task \"missing-q1\" not found in this backlog" -code: NOT_FOUND -help[1]: Run `tasks-axi list --file=.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` to see existing tasks -exit_code: 1 - -$ pnpm exec tsx bin/tasks-axi.ts ready --file .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -count: 3 -ready[3]{id,state,kind,repo,title}: - ready-q1,queued,task,"-",ready task - expired-q1,queued,task,"-",expired hold task - prose-held-q1,queued,task,"-",HELD prose marker should stay human prose -help[1]: - - Run `tasks-axi start --file=.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` to dispatch one of these - -$ pnpm exec tsx bin/tasks-axi.ts ready --include-held --file .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -count: 3 -ready[3]{id,state,kind,repo,title}: - ready-q1,queued,task,"-",ready task - expired-q1,queued,task,"-",expired hold task - prose-held-q1,queued,task,"-",HELD prose marker should stay human prose -held[2]{id,state,kind,repo,title,hold_reason,hold_kind,hold_until}: - held-q1,queued,task,"-",paused task,captain decision pending,captain,2999-01-01 - future-q1,queued,task,"-",future gated task,start after launch,future,2999-01-01 -help[1]: - - Run `tasks-axi start --file=.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` to dispatch one of these - -$ pnpm exec tsx bin/tasks-axi.ts list --state held --fields held,hold_reason,hold_kind,hold_until --file .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -count: 2 -tasks[2]{id,state,kind,repo,title,held,hold_reason,hold_kind,hold_until}: - held-q1,queued,task,"-",paused task,yes,captain decision pending,captain,2999-01-01 - future-q1,queued,task,"-",future gated task,yes,start after launch,future,2999-01-01 -help[1]: - - Run `tasks-axi show --file=.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` for full notes on a task - -$ pnpm exec tsx bin/tasks-axi.ts unhold held-q1 --file .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -ok: unhold held-q1 -> cleared -task: - id: held-q1 - title: paused task - state: queued - blocked: no - blocked_by: none - held: no - hold_reason: "-" - hold_kind: "-" - hold_until: "-" - kind: task - repo: "-" - priority: "-" - created: "-" - closed: "-" - deps: none - links: none - body: "" -help[1]: - - Run `tasks-axi ready --file=.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` to see dispatchable work - -$ pnpm exec tsx bin/tasks-axi.ts unhold held-q1 --json --file .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -{ - "ok": true, - "action": "unhold", - "already": true, - "task": { - "id": "held-q1", - "title": "paused task", - "state": "queued", - "kind": null, - "repo": null, - "priority": null, - "created": null, - "closed": null, - "deps": [], - "hold": null, - "links": [], - "body": null, - "blocked": false, - "blocked_by": [], - "held": false - } -} - -$ pnpm exec tsx bin/tasks-axi.ts ready --file .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -count: 4 -ready[4]{id,state,kind,repo,title}: - ready-q1,queued,task,"-",ready task - held-q1,queued,task,"-",paused task - expired-q1,queued,task,"-",expired hold task - prose-held-q1,queued,task,"-",HELD prose marker should stay human prose -help[1]: - - Run `tasks-axi start --file=.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md` to dispatch one of these - -$ sed -n '1,80p' .no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md -# Backlog - -## In flight -- [ ] active-blocker - refactor blocker - -## Queued -- [ ] ready-q1 - ready task -- [ ] held-q1 - paused task -- [ ] future-q1 - future gated task (hold: start after launch) (hold-kind: future) (hold-until: 2999-01-01) -- [ ] expired-q1 - expired hold task (hold: launch has passed) (hold-kind: future) (hold-until: 2000-01-01) -- [ ] blocked-q1 - blocked task blocked-by: active-blocker - waits for refactor -- [ ] prose-held-q1 - HELD prose marker should stay human prose - -## Done -- [x] done-q1 - finished - diff --git a/.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md b/.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md deleted file mode 100644 index c6ab5fc..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-hold-state/hold-state-e2e.backlog.md +++ /dev/null @@ -1,15 +0,0 @@ -# Backlog - -## In flight -- [ ] active-blocker - refactor blocker - -## Queued -- [ ] ready-q1 - ready task -- [ ] held-q1 - paused task -- [ ] future-q1 - future gated task (hold: start after launch) (hold-kind: future) (hold-until: 2999-01-01) -- [ ] expired-q1 - expired hold task (hold: launch has passed) (hold-kind: future) (hold-until: 2000-01-01) -- [ ] blocked-q1 - blocked task blocked-by: active-blocker - waits for refactor -- [ ] prose-held-q1 - HELD prose marker should stay human prose - -## Done -- [x] done-q1 - finished diff --git a/.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e-transcript.md b/.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e-transcript.md deleted file mode 100644 index 0be48dd..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e-transcript.md +++ /dev/null @@ -1,139 +0,0 @@ -# tasks-axi CLI E2E transcript - -Scenario: exercise the markdown backend through the shipped CLI, including caller-supplied IDs, dependency validation, ready filtering, already-done metadata backfill, archive-on-prune, and preservation of free-form markdown lines. - -```sh -$ pnpm exec tsx bin/tasks-axi.ts list --file .no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md --state queued -count: 3 -tasks[3]{id,state,kind,repo,title}: - lease-adopt,queued,task,acme,adopt the durable lease in the spin-up path - release-validation,queued,task,builder,"staged promote of builder v1.30.1 (target; cut as prerelease via #308 merge 2026\n... (truncated, 148 chars total - use show release-validation --full to see complete text)" - cert-cleanup,queued,task,monorepo,"port the post-upload cert pruning to the release workflow. Keep newest 2, never \n... (truncated, 105 chars total - use show cert-cleanup --full to see complete text)" -help[1]: - - Run `tasks-axi show --file=.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md` for full notes on a task -exit: 0 -``` - -```sh -$ pnpm exec tsx bin/tasks-axi.ts block cert-cleanup --by missing-q1 --file .no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md -error: "blocker \"missing-q1\" not found" -code: VALIDATION_ERROR -help[1]: "Create the blocker task first, or choose an existing task id" -exit: 2 -``` - -```sh -$ pnpm exec tsx bin/tasks-axi.ts block cert-cleanup --by owns-widget-h7 --file .no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md -block: - id: cert-cleanup - blocked_by: owns-widget-h7 -help[2]: - - Run `tasks-axi unblock cert-cleanup --by --file=.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md` to clear it - - Run `tasks-axi ready --file=.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md` to see what is still dispatchable -exit: 0 -``` - -```sh -$ pnpm exec tsx bin/tasks-axi.ts add release-notes-q1 publish\ release\ notes --kind docs --repo acme --blocked-by owns-widget-h7 --file .no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md -task: - id: release-notes-q1 - title: publish release notes - state: queued - blocked: yes - blocked_by: owns-widget-h7 - kind: docs - repo: acme - priority: "-" - created: 2026-06-23 - closed: "-" - deps: "blocked-by:owns-widget-h7" - links: none - body: "" -help[2]: - - Run `tasks-axi start release-notes-q1 --file=.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md` to move it to in flight - - Run `tasks-axi block release-notes-q1 --by --file=.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md` to record a dependency -exit: 0 -``` - -```sh -$ pnpm exec tsx bin/tasks-axi.ts ready --file .no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md -count: 2 -ready[2]{id,state,kind,repo,title}: - lease-adopt,queued,task,acme,adopt the durable lease in the spin-up path - release-validation,queued,task,builder,"staged promote of builder v1.30.1 (target; cut as prerelease via #308 merge 2026\n... (truncated, 148 chars total - use show release-validation --full to see complete text)" -help[1]: - - Run `tasks-axi start --file=.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md` to dispatch one of these -exit: 0 -``` - -```sh -$ pnpm exec tsx bin/tasks-axi.ts done lease-core-t4 --pr https://github.com/acme/builder/pull/77 --note backfilled\ review\ evidence --no-prune --file .no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md -already: true -done: - id: lease-core-t4 - state: done - pruned: 0 -task: - id: lease-core-t4 - title: "SHIP MERGED https://github.com/acme/builder/pull/35 (squash, 2026-06-22): durabl\n... (truncated, 189 chars total - use show lease-core-t4 --full to see complete text)" - state: done - blocked: no - blocked_by: none - kind: ship - repo: "-" - priority: "-" - created: "-" - closed: 2026-06-22 - deps: none - links: "pr:https://github.com/acme/builder/pull/35,pr:https://github.com/acme/builder/pull/77" - body: backfilled review evidence -exit: 0 -``` - -```sh -$ pnpm exec tsx bin/tasks-axi.ts done cert-cleanup --pr https://github.com/acme/monorepo/pull/91 --keep 2 --file .no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md -done: - id: cert-cleanup - state: done - pruned: 3 -help[1]: - - Run `tasks-axi ready --file=.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md` to dispatch work unblocked by this -exit: 0 -``` - -## Persisted backlog after CLI mutations - -```md -# Backlog - -## In flight -- **owns-widget-h7** - PERSISTENT SECONDMATE (kind=secondmate, home ~/work/widget, harness claude). Owns the widget end to end including the release cycle, CI, and the store review watch. Idle pane is healthy; supervised by status writes. (since 2026-06-22) -- **lease-adopt-l5** - SHIP (acme, builder): adopt durable lease in spin-up - acquire home via `builder get --lease` and release on retirement. (since 2026-06-22). **pipeline running.** -- Release domain (owned by owns-widget-h7): 1.0.4 IN REVIEW; next release held. Full detail in the secondmate home. -- (status) Mobile ladder: 0.0.4 confirmed good on-device 2026-06-21. More rough edges to flag later. - -## Queued -- [ ] lease-adopt - adopt the durable lease in the spin-up path blocked-by: lease-core-t4 (repo: acme) -- [ ] release-validation - staged promote of builder v1.30.1 (target; cut as prerelease via #308 merge 2026-06-21 - carries fork-fix). REMAINING: validate then flip to latest. (local + repo: builder) -- [ ] go-live (CAPTAIN-GATED) - full launch checklist is the SINGLE SOURCE OF TRUTH at data/go-live.md. Discrete tasks spawn from there. -- [ ] (later roadmap, data/pivot-plan.md) Phase 6 hardening remainder, multi-bot concurrency. - -- [ ] release-notes-q1 - publish release notes blocked-by: owns-widget-h7 (repo: acme) (kind: docs) (since 2026-06-23) -## Done (10 most recent) -- [x] cert-cleanup - port the post-upload cert pruning to the release workflow. Keep newest 2, never touch distribution certs. https://github.com/acme/monorepo/pull/91 blocked-by: owns-widget-h7 (repo: monorepo) (merged 2026-06-23) -- [x] design-scout-d4 - SCOUT - data/design-scout-d4/report.md (reported 2026-06-22): design assessment for a backlog CLI. VERDICT BUILD-MVP-FIRST: own repo, markdown backend owning backlog.md in place. (reported 2026-06-22) -- [x] PR #31 (contributor) - SHIP MERGED https://github.com/acme/builder/pull/31 (squash, 2026-06-22): teardown treats work as landed when HEAD is on any remote. Reviewed MERGE AS-IS, contributor thanked. (merged 2026-06-22) -``` - -## Archive created by done auto-prune - -```md - -## Archived 2026-06-23 -- [x] lease-core-t4 - SHIP MERGED https://github.com/acme/builder/pull/35 (squash, 2026-06-22): durable worktree lease with persistent state independent of live processes. https://github.com/acme/builder/pull/77 (merged 2026-06-22) - backfilled review evidence -- [x] fork-fix-k4 - SHIP (builder, PR https://github.com/acme/builder/pull/306, captain-merged, released v1.30.0): clean-slate fork-routing fix. Superseded contributor #296 (closed w/ link). Unblocks the fork-contribution flow. (merged 2026-06-21) -- [x] multi-line-w8 - SCOUT - data/multi-line-w8/report.md: isolated e2e of the feature. - Follow-up note added later: the harness env-plumbing was the cause, not the feature. - Second continuation line for good measure. (reported 2026-06-22) -``` diff --git a/.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md b/.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md deleted file mode 100644 index cf3f8ff..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/backlog.md +++ /dev/null @@ -1,19 +0,0 @@ -# Backlog - -## In flight -- **owns-widget-h7** - PERSISTENT SECONDMATE (kind=secondmate, home ~/work/widget, harness claude). Owns the widget end to end including the release cycle, CI, and the store review watch. Idle pane is healthy; supervised by status writes. (since 2026-06-22) -- **lease-adopt-l5** - SHIP (acme, builder): adopt durable lease in spin-up - acquire home via `builder get --lease` and release on retirement. (since 2026-06-22). **pipeline running.** -- Release domain (owned by owns-widget-h7): 1.0.4 IN REVIEW; next release held. Full detail in the secondmate home. -- (status) Mobile ladder: 0.0.4 confirmed good on-device 2026-06-21. More rough edges to flag later. - -## Queued -- [ ] lease-adopt - adopt the durable lease in the spin-up path blocked-by: lease-core-t4 (repo: acme) -- [ ] release-validation - staged promote of builder v1.30.1 (target; cut as prerelease via #308 merge 2026-06-21 - carries fork-fix). REMAINING: validate then flip to latest. (local + repo: builder) -- [ ] go-live (CAPTAIN-GATED) - full launch checklist is the SINGLE SOURCE OF TRUTH at data/go-live.md. Discrete tasks spawn from there. -- [ ] (later roadmap, data/pivot-plan.md) Phase 6 hardening remainder, multi-bot concurrency. - -- [ ] release-notes-q1 - publish release notes blocked-by: owns-widget-h7 (repo: acme) (kind: docs) (since 2026-06-23) -## Done (10 most recent) -- [x] cert-cleanup - port the post-upload cert pruning to the release workflow. Keep newest 2, never touch distribution certs. https://github.com/acme/monorepo/pull/91 blocked-by: owns-widget-h7 (repo: monorepo) (merged 2026-06-23) -- [x] design-scout-d4 - SCOUT - data/design-scout-d4/report.md (reported 2026-06-22): design assessment for a backlog CLI. VERDICT BUILD-MVP-FIRST: own repo, markdown backend owning backlog.md in place. (reported 2026-06-22) -- [x] PR #31 (contributor) - SHIP MERGED https://github.com/acme/builder/pull/31 (squash, 2026-06-22): teardown treats work as landed when HEAD is on any remote. Reviewed MERGE AS-IS, contributor thanked. (merged 2026-06-22) diff --git a/.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/done-archive.md b/.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/done-archive.md deleted file mode 100644 index 7b840e7..0000000 --- a/.no-mistakes/evidence/fm/tasks-axi-p1-b8/cli-e2e/done-archive.md +++ /dev/null @@ -1,8 +0,0 @@ - -## Archived 2026-06-23 -- [x] lease-core-t4 - SHIP MERGED https://github.com/acme/builder/pull/35 (squash, 2026-06-22): durable worktree lease with persistent state independent of live processes. https://github.com/acme/builder/pull/77 (merged 2026-06-22) - backfilled review evidence -- [x] fork-fix-k4 - SHIP (builder, PR https://github.com/acme/builder/pull/306, captain-merged, released v1.30.0): clean-slate fork-routing fix. Superseded contributor #296 (closed w/ link). Unblocks the fork-contribution flow. (merged 2026-06-21) -- [x] multi-line-w8 - SCOUT - data/multi-line-w8/report.md: isolated e2e of the feature. - Follow-up note added later: the harness env-plumbing was the cause, not the feature. - Second continuation line for good measure. (reported 2026-06-22) diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md b/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md deleted file mode 100644 index 8686130..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md +++ /dev/null @@ -1,8 +0,0 @@ -# Destination backlog - -## In flight - -## Queued -- [ ] ordinary-q1 - Same-home operational blocker - -## Done diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-template.md b/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-template.md deleted file mode 100644 index 8686130..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-template.md +++ /dev/null @@ -1,8 +0,0 @@ -# Destination backlog - -## In flight - -## Queued -- [ ] ordinary-q1 - Same-home operational blocker - -## Done diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/done-archive.md b/.no-mistakes/evidence/fm/tasks-public-followup-k4/done-archive.md deleted file mode 100644 index 051e7a1..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/done-archive.md +++ /dev/null @@ -1,4 +0,0 @@ - -## Archived 2026-07-13 -- [x] public-final-ab - Publish the public-safe result after the fix lands (kind: public-followup) (done 2026-07-13) - diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/e2e-transcript.log b/.no-mistakes/evidence/fm/tasks-public-followup-k4/e2e-transcript.log deleted file mode 100644 index 1b6dac9..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/e2e-transcript.log +++ /dev/null @@ -1,1043 +0,0 @@ - -$ tasks-axi public-followup add public-final-ab --request-context-file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/request.json --purpose promised-final --expected-final-file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/expected.json --expires-at 2026-10-01T00:00:00Z --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/source-backlog.md --json -{ - "ok": true, - "action": "public-followup.add", - "revision": 1, - "changed": [ - "task", - "public_followup" - ], - "task": { - "id": "public-final-ab", - "title": "Publish the public-safe result after the fix lands", - "state": "queued", - "kind": "public-followup", - "repo": null, - "priority": null, - "created": "2026-07-13", - "closed": null, - "deps": [], - "hold": null, - "links": [], - "body": null, - "public_followup": { - "schema_version": 1, - "revision": 1, - "request": { - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" - }, - "purpose": "promised-final", - "expected_final": { - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" - }, - "obligation_expires_at": "2026-10-01T00:00:00Z", - "delivery": { - "state": "intent", - "delivery_key": "fd1_N_PasuCJajWSw1bgu4QBIkfwqxSsnEtd", - "payload_digest": null, - "attempt_count": 0, - "last_error_code": null, - "next_attempt_at": null, - "receipt": null, - "last_error": null, - "waiver": null - }, - "work_relations": [], - "lineage": { - "predecessor_obligation_id": null, - "successor_obligation_id": null - } - }, - "blocked": false, - "blocked_by": [], - "held": false - } -} - -$ tasks-axi public-followup bind-work public-final-ab --relation-file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/relation.json --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/source-backlog.md --json -{ - "ok": true, - "action": "public-followup.bind-work", - "revision": 2, - "changed": [ - "work_relations", - "delivery.state" - ], - "task": { - "id": "public-final-ab", - "title": "Publish the public-safe result after the fix lands", - "state": "queued", - "kind": "public-followup", - "repo": null, - "priority": null, - "created": "2026-07-13", - "closed": null, - "deps": [], - "hold": null, - "links": [], - "body": null, - "public_followup": { - "schema_version": 1, - "revision": 2, - "request": { - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" - }, - "purpose": "promised-final", - "expected_final": { - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" - }, - "obligation_expires_at": "2026-10-01T00:00:00Z", - "delivery": { - "state": "pending-work", - "delivery_key": "fd1_N_PasuCJajWSw1bgu4QBIkfwqxSsnEtd", - "payload_digest": null, - "attempt_count": 0, - "last_error_code": null, - "next_attempt_at": null, - "receipt": null, - "last_error": null, - "waiver": null - }, - "work_relations": [ - { - "relation_id": "rel-code", - "work_ref": { - "home_id": "secondmate:demo", - "task_id": "work-code-q1" - }, - "role": "fulfills", - "required": true, - "generation": 1, - "state": "bound", - "successor_relation_id": null, - "accepted_event_ids": [], - "accepted_events": [] - } - ], - "lineage": { - "predecessor_obligation_id": null, - "successor_obligation_id": null - } - }, - "blocked": false, - "blocked_by": [], - "held": false - } -} - -$ tasks-axi mv public-final-ab --to /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/source-backlog.md --json -{ - "ok": true, - "action": "mv", - "id": "public-final-ab", - "from": "/Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/source-backlog.md", - "to": "/Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md" -} - -$ tasks-axi public-followup list --work-ref secondmate:demo/work-code-q1 --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -{ - "ok": true, - "action": "public-followup.list", - "count": 1, - "public_followups": [ - { - "id": "public-final-ab", - "title": "Publish the public-safe result after the fix lands", - "state": "queued", - "kind": "public-followup", - "repo": null, - "priority": null, - "created": "2026-07-13", - "closed": null, - "deps": [], - "hold": null, - "links": [], - "body": null, - "public_followup": { - "schema_version": 1, - "revision": 2, - "request": { - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" - }, - "purpose": "promised-final", - "expected_final": { - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" - }, - "obligation_expires_at": "2026-10-01T00:00:00Z", - "delivery": { - "state": "pending-work", - "delivery_key": "fd1_N_PasuCJajWSw1bgu4QBIkfwqxSsnEtd", - "payload_digest": null, - "attempt_count": 0, - "last_error_code": null, - "next_attempt_at": null, - "receipt": null, - "last_error": null, - "waiver": null - }, - "work_relations": [ - { - "relation_id": "rel-code", - "work_ref": { - "home_id": "secondmate:demo", - "task_id": "work-code-q1" - }, - "role": "fulfills", - "required": true, - "generation": 1, - "state": "bound", - "successor_relation_id": null, - "accepted_event_ids": [], - "accepted_events": [] - } - ], - "lineage": { - "predecessor_obligation_id": null, - "successor_obligation_id": null - } - }, - "blocked": false, - "blocked_by": [], - "held": false - } - ] -} - -$ tasks-axi public-followup work-event public-final-ab --event-file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/event.json --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -{ - "ok": true, - "action": "public-followup.work-event", - "revision": 3, - "changed": [ - "work_relations", - "delivery.state" - ], - "task": { - "id": "public-final-ab", - "title": "Publish the public-safe result after the fix lands", - "state": "queued", - "kind": "public-followup", - "repo": null, - "priority": null, - "created": "2026-07-13", - "closed": null, - "deps": [], - "hold": null, - "links": [], - "body": null, - "public_followup": { - "schema_version": 1, - "revision": 3, - "request": { - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" - }, - "purpose": "promised-final", - "expected_final": { - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" - }, - "obligation_expires_at": "2026-10-01T00:00:00Z", - "delivery": { - "state": "ready", - "delivery_key": "fd1_N_PasuCJajWSw1bgu4QBIkfwqxSsnEtd", - "payload_digest": null, - "attempt_count": 0, - "last_error_code": null, - "next_attempt_at": null, - "receipt": null, - "last_error": null, - "waiver": null - }, - "work_relations": [ - { - "relation_id": "rel-code", - "work_ref": { - "home_id": "secondmate:demo", - "task_id": "work-code-q1" - }, - "role": "fulfills", - "required": true, - "generation": 1, - "state": "landed", - "successor_relation_id": null, - "accepted_event_ids": [ - "evt-code-landed" - ], - "accepted_events": [ - { - "schema_version": 1, - "event_id": "evt-code-landed", - "obligation_id": "public-final-ab", - "relation_id": "rel-code", - "generation": 1, - "source_home_id": "secondmate:demo", - "work_id": "work-code-q1", - "outcome_type": "pr-merged", - "deliverables": { - "pr_url": "https://github.com/o/r/pull/519" - }, - "public_safe_outcome": "The fix merged in PR 519.", - "occurred_at": "2026-07-14T12:00:00Z" - } - ] - } - ], - "lineage": { - "predecessor_obligation_id": null, - "successor_obligation_id": null - } - }, - "blocked": false, - "blocked_by": [], - "held": false - } -} - -$ tasks-axi public-followup work-event public-final-ab --event-file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/event.json --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -{ - "ok": true, - "action": "public-followup.work-event", - "already": true, - "revision": 3, - "changed": [], - "task": { - "id": "public-final-ab", - "title": "Publish the public-safe result after the fix lands", - "state": "queued", - "kind": "public-followup", - "repo": null, - "priority": null, - "created": "2026-07-13", - "closed": null, - "deps": [], - "hold": null, - "links": [], - "body": null, - "public_followup": { - "schema_version": 1, - "revision": 3, - "request": { - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" - }, - "purpose": "promised-final", - "expected_final": { - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" - }, - "obligation_expires_at": "2026-10-01T00:00:00Z", - "delivery": { - "state": "ready", - "delivery_key": "fd1_N_PasuCJajWSw1bgu4QBIkfwqxSsnEtd", - "payload_digest": null, - "attempt_count": 0, - "last_error_code": null, - "next_attempt_at": null, - "receipt": null, - "last_error": null, - "waiver": null - }, - "work_relations": [ - { - "relation_id": "rel-code", - "work_ref": { - "home_id": "secondmate:demo", - "task_id": "work-code-q1" - }, - "role": "fulfills", - "required": true, - "generation": 1, - "state": "landed", - "successor_relation_id": null, - "accepted_event_ids": [ - "evt-code-landed" - ], - "accepted_events": [ - { - "schema_version": 1, - "event_id": "evt-code-landed", - "obligation_id": "public-final-ab", - "relation_id": "rel-code", - "generation": 1, - "source_home_id": "secondmate:demo", - "work_id": "work-code-q1", - "outcome_type": "pr-merged", - "deliverables": { - "pr_url": "https://github.com/o/r/pull/519" - }, - "public_safe_outcome": "The fix merged in PR 519.", - "occurred_at": "2026-07-14T12:00:00Z" - } - ] - } - ], - "lineage": { - "predecessor_obligation_id": null, - "successor_obligation_id": null - } - }, - "blocked": false, - "blocked_by": [], - "held": false - } -} - -$ tasks-axi block public-final-ab --by ordinary-q1 --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -{ - "ok": true, - "action": "block", - "blocked_by": "ordinary-q1", - "task": { - "id": "public-final-ab", - "title": "Publish the public-safe result after the fix lands", - "state": "queued", - "kind": "public-followup", - "repo": null, - "priority": null, - "created": "2026-07-13", - "closed": null, - "deps": [ - { - "type": "blocked-by", - "id": "ordinary-q1" - } - ], - "hold": null, - "links": [], - "body": null, - "public_followup": { - "schema_version": 1, - "revision": 3, - "request": { - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" - }, - "purpose": "promised-final", - "expected_final": { - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" - }, - "obligation_expires_at": "2026-10-01T00:00:00Z", - "delivery": { - "state": "ready", - "delivery_key": "fd1_N_PasuCJajWSw1bgu4QBIkfwqxSsnEtd", - "payload_digest": null, - "attempt_count": 0, - "last_error_code": null, - "next_attempt_at": null, - "receipt": null, - "last_error": null, - "waiver": null - }, - "work_relations": [ - { - "relation_id": "rel-code", - "work_ref": { - "home_id": "secondmate:demo", - "task_id": "work-code-q1" - }, - "role": "fulfills", - "required": true, - "generation": 1, - "state": "landed", - "successor_relation_id": null, - "accepted_event_ids": [ - "evt-code-landed" - ], - "accepted_events": [ - { - "schema_version": 1, - "event_id": "evt-code-landed", - "obligation_id": "public-final-ab", - "relation_id": "rel-code", - "generation": 1, - "source_home_id": "secondmate:demo", - "work_id": "work-code-q1", - "outcome_type": "pr-merged", - "deliverables": { - "pr_url": "https://github.com/o/r/pull/519" - }, - "public_safe_outcome": "The fix merged in PR 519.", - "occurred_at": "2026-07-14T12:00:00Z" - } - ] - } - ], - "lineage": { - "predecessor_obligation_id": null, - "successor_obligation_id": null - } - }, - "blocked": true, - "blocked_by": [ - "ordinary-q1" - ], - "held": false - } -} - -$ tasks-axi public-followup ready --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -{ - "ok": true, - "action": "public-followup.ready", - "count": 0, - "ready_public_followups": [] -} - -$ tasks-axi public-followup begin-delivery public-final-ab --payload-hash aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -error: Cannot begin delivery while the obligation has an active blocker -code: VALIDATION_ERROR -[exit 2, rejected as expected] - -$ tasks-axi unblock public-final-ab --by ordinary-q1 --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -{ - "ok": true, - "action": "unblock", - "blocked_by": "ordinary-q1", - "task": { - "id": "public-final-ab", - "title": "Publish the public-safe result after the fix lands", - "state": "queued", - "kind": "public-followup", - "repo": null, - "priority": null, - "created": "2026-07-13", - "closed": null, - "deps": [], - "hold": null, - "links": [], - "body": null, - "public_followup": { - "schema_version": 1, - "revision": 3, - "request": { - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" - }, - "purpose": "promised-final", - "expected_final": { - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" - }, - "obligation_expires_at": "2026-10-01T00:00:00Z", - "delivery": { - "state": "ready", - "delivery_key": "fd1_N_PasuCJajWSw1bgu4QBIkfwqxSsnEtd", - "payload_digest": null, - "attempt_count": 0, - "last_error_code": null, - "next_attempt_at": null, - "receipt": null, - "last_error": null, - "waiver": null - }, - "work_relations": [ - { - "relation_id": "rel-code", - "work_ref": { - "home_id": "secondmate:demo", - "task_id": "work-code-q1" - }, - "role": "fulfills", - "required": true, - "generation": 1, - "state": "landed", - "successor_relation_id": null, - "accepted_event_ids": [ - "evt-code-landed" - ], - "accepted_events": [ - { - "schema_version": 1, - "event_id": "evt-code-landed", - "obligation_id": "public-final-ab", - "relation_id": "rel-code", - "generation": 1, - "source_home_id": "secondmate:demo", - "work_id": "work-code-q1", - "outcome_type": "pr-merged", - "deliverables": { - "pr_url": "https://github.com/o/r/pull/519" - }, - "public_safe_outcome": "The fix merged in PR 519.", - "occurred_at": "2026-07-14T12:00:00Z" - } - ] - } - ], - "lineage": { - "predecessor_obligation_id": null, - "successor_obligation_id": null - } - }, - "blocked": false, - "blocked_by": [], - "held": false - } -} - -$ tasks-axi ready --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md -count: 1 -ready[1]{id,state,kind,repo,title}: - ordinary-q1,queued,task,"-",Same-home operational blocker -ready_public_followups[1]{id,state,kind,repo,title,delivery_state}: - public-final-ab,queued,public-followup,"-",Publish the public-safe result after the fix lands,ready -help[1]: - - Run `tasks-axi start --file=/Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md` to dispatch one of these - -$ tasks-axi public-followup ready --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -{ - "ok": true, - "action": "public-followup.ready", - "count": 1, - "ready_public_followups": [ - { - "id": "public-final-ab", - "title": "Publish the public-safe result after the fix lands", - "state": "queued", - "kind": "public-followup", - "repo": null, - "priority": null, - "created": "2026-07-13", - "closed": null, - "deps": [], - "hold": null, - "links": [], - "body": null, - "public_followup": { - "schema_version": 1, - "revision": 3, - "request": { - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" - }, - "purpose": "promised-final", - "expected_final": { - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" - }, - "obligation_expires_at": "2026-10-01T00:00:00Z", - "delivery": { - "state": "ready", - "delivery_key": "fd1_N_PasuCJajWSw1bgu4QBIkfwqxSsnEtd", - "payload_digest": null, - "attempt_count": 0, - "last_error_code": null, - "next_attempt_at": null, - "receipt": null, - "last_error": null, - "waiver": null - }, - "work_relations": [ - { - "relation_id": "rel-code", - "work_ref": { - "home_id": "secondmate:demo", - "task_id": "work-code-q1" - }, - "role": "fulfills", - "required": true, - "generation": 1, - "state": "landed", - "successor_relation_id": null, - "accepted_event_ids": [ - "evt-code-landed" - ], - "accepted_events": [ - { - "schema_version": 1, - "event_id": "evt-code-landed", - "obligation_id": "public-final-ab", - "relation_id": "rel-code", - "generation": 1, - "source_home_id": "secondmate:demo", - "work_id": "work-code-q1", - "outcome_type": "pr-merged", - "deliverables": { - "pr_url": "https://github.com/o/r/pull/519" - }, - "public_safe_outcome": "The fix merged in PR 519.", - "occurred_at": "2026-07-14T12:00:00Z" - } - ] - } - ], - "lineage": { - "predecessor_obligation_id": null, - "successor_obligation_id": null - } - }, - "blocked": false, - "blocked_by": [], - "held": false - } - ] -} - -$ tasks-axi start public-final-ab --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -error: Public-followup state cannot change through generic transitions -code: VALIDATION_ERROR -help[1]: Use `tasks-axi public-followup record-delivery` or `tasks-axi public-followup waive` -[exit 2, rejected as expected] - -$ tasks-axi done public-final-ab --no-prune --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -error: Public-followup state cannot change through generic transitions -code: VALIDATION_ERROR -help[1]: Use `tasks-axi public-followup record-delivery` or `tasks-axi public-followup waive` -[exit 2, rejected as expected] - -$ tasks-axi hold public-final-ab --reason unsafe-dispatch-hold --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -error: Public-followup content and holds cannot change through generic update -code: VALIDATION_ERROR -help[1]: Create a successor obligation when the public promise changes -[exit 2, rejected as expected] - -$ tasks-axi public-followup begin-delivery public-final-ab --payload-hash aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -{ - "ok": true, - "action": "public-followup.begin-delivery", - "revision": 4, - "changed": [ - "delivery" - ], - "task": { - "id": "public-final-ab", - "title": "Publish the public-safe result after the fix lands", - "state": "queued", - "kind": "public-followup", - "repo": null, - "priority": null, - "created": "2026-07-13", - "closed": null, - "deps": [], - "hold": null, - "links": [], - "body": null, - "public_followup": { - "schema_version": 1, - "revision": 4, - "request": { - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" - }, - "purpose": "promised-final", - "expected_final": { - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" - }, - "obligation_expires_at": "2026-10-01T00:00:00Z", - "delivery": { - "state": "delivery-posting", - "delivery_key": "fd1_N_PasuCJajWSw1bgu4QBIkfwqxSsnEtd", - "payload_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "attempt_count": 1, - "last_error_code": null, - "next_attempt_at": null, - "receipt": null, - "last_error": null, - "waiver": null - }, - "work_relations": [ - { - "relation_id": "rel-code", - "work_ref": { - "home_id": "secondmate:demo", - "task_id": "work-code-q1" - }, - "role": "fulfills", - "required": true, - "generation": 1, - "state": "landed", - "successor_relation_id": null, - "accepted_event_ids": [ - "evt-code-landed" - ], - "accepted_events": [ - { - "schema_version": 1, - "event_id": "evt-code-landed", - "obligation_id": "public-final-ab", - "relation_id": "rel-code", - "generation": 1, - "source_home_id": "secondmate:demo", - "work_id": "work-code-q1", - "outcome_type": "pr-merged", - "deliverables": { - "pr_url": "https://github.com/o/r/pull/519" - }, - "public_safe_outcome": "The fix merged in PR 519.", - "occurred_at": "2026-07-14T12:00:00Z" - } - ] - } - ], - "lineage": { - "predecessor_obligation_id": null, - "successor_obligation_id": null - } - }, - "blocked": false, - "blocked_by": [], - "held": false - } -} - -$ tasks-axi public-followup record-delivery public-final-ab --receipt-file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/receipt.json --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -{ - "ok": true, - "action": "public-followup.record-delivery", - "revision": 5, - "changed": [ - "delivery", - "state" - ], - "pruned": 0, - "task": { - "id": "public-final-ab", - "title": "Publish the public-safe result after the fix lands", - "state": "done", - "kind": "public-followup", - "repo": null, - "priority": null, - "created": "2026-07-13", - "closed": "2026-07-13", - "deps": [], - "hold": null, - "links": [], - "body": null, - "public_followup": { - "schema_version": 1, - "revision": 5, - "request": { - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" - }, - "purpose": "promised-final", - "expected_final": { - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" - }, - "obligation_expires_at": "2026-10-01T00:00:00Z", - "delivery": { - "state": "posted", - "delivery_key": "fd1_N_PasuCJajWSw1bgu4QBIkfwqxSsnEtd", - "payload_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "attempt_count": 1, - "last_error_code": null, - "next_attempt_at": null, - "receipt": { - "schema_version": 1, - "state": "posted", - "request_id": "req-e2e-public-final", - "platform": "discord", - "attempt_count": 1, - "total_chunks": 1, - "posted_chunks": 1, - "posted_at": "2026-07-14T13:00:00Z", - "retain_until": "2026-10-14T13:00:00Z" - }, - "last_error": null, - "waiver": null - }, - "work_relations": [ - { - "relation_id": "rel-code", - "work_ref": { - "home_id": "secondmate:demo", - "task_id": "work-code-q1" - }, - "role": "fulfills", - "required": true, - "generation": 1, - "state": "landed", - "successor_relation_id": null, - "accepted_event_ids": [ - "evt-code-landed" - ], - "accepted_events": [ - { - "schema_version": 1, - "event_id": "evt-code-landed", - "obligation_id": "public-final-ab", - "relation_id": "rel-code", - "generation": 1, - "source_home_id": "secondmate:demo", - "work_id": "work-code-q1", - "outcome_type": "pr-merged", - "deliverables": { - "pr_url": "https://github.com/o/r/pull/519" - }, - "public_safe_outcome": "The fix merged in PR 519.", - "occurred_at": "2026-07-14T12:00:00Z" - } - ] - } - ], - "lineage": { - "predecessor_obligation_id": null, - "successor_obligation_id": null - } - }, - "blocked": false, - "blocked_by": [], - "held": false - } -} - -$ tasks-axi reopen public-final-ab --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -error: Public-followup state cannot change through generic transitions -code: VALIDATION_ERROR -help[1]: Use `tasks-axi public-followup record-delivery` or `tasks-axi public-followup waive` -[exit 2, rejected as expected] - -$ tasks-axi prune --state done --keep 0 --file /Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KXEK7C9NZSYF26KG0243B871/.no-mistakes/evidence/fm/tasks-public-followup-k4/destination-backlog.md --json -{ - "ok": true, - "action": "prune", - "state": "done", - "kept": 0, - "archived": 1, - "ids": [ - "public-final-ab" - ] -} - -$ decode archived reserved metadata -{ - "schema_version": 1, - "revision": 5, - "delivery_state": "posted", - "receipt_state": "posted", - "request_id": "req-e2e-public-final", - "accepted_event_ids": [ - "evt-code-landed" - ] -} diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/e2e.sh b/.no-mistakes/evidence/fm/tasks-public-followup-k4/e2e.sh deleted file mode 100644 index fb8dbd1..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/e2e.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)" -EVIDENCE="$ROOT/.no-mistakes/evidence/fm/tasks-public-followup-k4" -SOURCE="$EVIDENCE/source-backlog.md" -DESTINATION="$EVIDENCE/destination-backlog.md" -CLI=(pnpm --silent dev) - -cp "$EVIDENCE/source-template.md" "$SOURCE" -cp "$EVIDENCE/destination-template.md" "$DESTINATION" -rm -f "$EVIDENCE/done-archive.md" - -run() { - printf '\n$ tasks-axi' - printf ' %q' "$@" - printf '\n' - "${CLI[@]}" "$@" -} - -reject() { - printf '\n$ tasks-axi' - printf ' %q' "$@" - printf '\n' - set +e - "${CLI[@]}" "$@" 2>&1 - status=$? - set -e - printf '[exit %s, rejected as expected]\n' "$status" - test "$status" -ne 0 -} - -run public-followup add public-final-ab \ - --request-context-file "$EVIDENCE/request.json" \ - --purpose promised-final \ - --expected-final-file "$EVIDENCE/expected.json" \ - --expires-at 2026-10-01T00:00:00Z \ - --file "$SOURCE" --json - -run public-followup bind-work public-final-ab \ - --relation-file "$EVIDENCE/relation.json" \ - --file "$SOURCE" --json - -run mv public-final-ab --to "$DESTINATION" --file "$SOURCE" --json - -run public-followup list \ - --work-ref secondmate:demo/work-code-q1 \ - --file "$DESTINATION" --json - -run public-followup work-event public-final-ab \ - --event-file "$EVIDENCE/event.json" \ - --file "$DESTINATION" --json - -run public-followup work-event public-final-ab \ - --event-file "$EVIDENCE/event.json" \ - --file "$DESTINATION" --json - -run block public-final-ab --by ordinary-q1 --file "$DESTINATION" --json -run public-followup ready --file "$DESTINATION" --json -reject public-followup begin-delivery public-final-ab \ - --payload-hash aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ - --file "$DESTINATION" --json - -run unblock public-final-ab --by ordinary-q1 --file "$DESTINATION" --json -run ready --file "$DESTINATION" -run public-followup ready --file "$DESTINATION" --json - -reject start public-final-ab --file "$DESTINATION" --json -reject done public-final-ab --no-prune --file "$DESTINATION" --json -reject hold public-final-ab --reason unsafe-dispatch-hold --file "$DESTINATION" --json - -run public-followup begin-delivery public-final-ab \ - --payload-hash aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ - --file "$DESTINATION" --json - -run public-followup record-delivery public-final-ab \ - --receipt-file "$EVIDENCE/receipt.json" \ - --file "$DESTINATION" --json - -reject reopen public-final-ab --file "$DESTINATION" --json -run prune --state done --keep 0 --file "$DESTINATION" --json - -printf '\n$ decode archived reserved metadata\n' -node -e ' - const fs = require("fs"); - const text = fs.readFileSync(process.argv[1], "utf8"); - const encoded = text.match(/tasks-axi:public-followup\/v1:([A-Za-z0-9_-]+)/)?.[1]; - if (!encoded) throw new Error("archived metadata missing"); - const value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")); - console.log(JSON.stringify({ - schema_version: value.schema_version, - revision: value.revision, - delivery_state: value.delivery.state, - receipt_state: value.delivery.receipt.state, - request_id: value.delivery.receipt.request_id, - accepted_event_ids: value.work_relations[0].accepted_event_ids - }, null, 2)); -' "$EVIDENCE/done-archive.md" diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/event.json b/.no-mistakes/evidence/fm/tasks-public-followup-k4/event.json deleted file mode 100644 index 191a2cb..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/event.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "event_id": "evt-code-landed", - "obligation_id": "public-final-ab", - "relation_id": "rel-code", - "generation": 1, - "source_home_id": "secondmate:demo", - "work_id": "work-code-q1", - "outcome_type": "pr-merged", - "deliverables": { - "pr_url": "https://github.com/o/r/pull/519" - }, - "public_safe_outcome": "The fix merged in PR 519.", - "successor": null, - "occurred_at": "2026-07-14T12:00:00Z" -} diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/expected.json b/.no-mistakes/evidence/fm/tasks-public-followup-k4/expected.json deleted file mode 100644 index 87cfe67..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/expected.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "pr-merged", - "project": "demo", - "required_deliverables": [ - "pr_url" - ], - "completion_policy": "all-required" -} diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/receipt.json b/.no-mistakes/evidence/fm/tasks-public-followup-k4/receipt.json deleted file mode 100644 index 26d2d5c..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/receipt.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schema_version": 1, - "state": "posted", - "request_id": "req-e2e-public-final", - "platform": "discord", - "attempt_count": 1, - "total_chunks": 1, - "posted_chunks": 1, - "posted_at": "2026-07-14T13:00:00Z", - "retain_until": "2026-10-14T13:00:00Z" -} diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/relation.json b/.no-mistakes/evidence/fm/tasks-public-followup-k4/relation.json deleted file mode 100644 index 37ec150..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/relation.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "relation_id": "rel-code", - "work_ref": { - "home_id": "secondmate:demo", - "task_id": "work-code-q1" - }, - "role": "fulfills", - "required": true, - "generation": 1 -} diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/request.json b/.no-mistakes/evidence/fm/tasks-public-followup-k4/request.json deleted file mode 100644 index 7dbfb52..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/request.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "request_id": "req-e2e-public-final", - "platform": "discord", - "context_binding": { - "version": "ctx1", - "value": "ctx1_e2e_opaque" - }, - "public_safe_summary": "Publish the public-safe result after the fix lands", - "received_at": "2026-07-13T12:00:00Z", - "followup_expires_at": "2026-08-13T12:00:00Z", - "reservation_expires_at": "2026-09-13T12:00:00Z" -} diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/source-backlog.md b/.no-mistakes/evidence/fm/tasks-public-followup-k4/source-backlog.md deleted file mode 100644 index 2f4b892..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/source-backlog.md +++ /dev/null @@ -1,6 +0,0 @@ -# Source backlog - -## In flight - -## Queued -## Done diff --git a/.no-mistakes/evidence/fm/tasks-public-followup-k4/source-template.md b/.no-mistakes/evidence/fm/tasks-public-followup-k4/source-template.md deleted file mode 100644 index 4cdae11..0000000 --- a/.no-mistakes/evidence/fm/tasks-public-followup-k4/source-template.md +++ /dev/null @@ -1,7 +0,0 @@ -# Source backlog - -## In flight - -## Queued - -## Done diff --git a/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/cli-transcript.txt b/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/cli-transcript.txt deleted file mode 100644 index ce35ba5..0000000 --- a/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/cli-transcript.txt +++ /dev/null @@ -1,29 +0,0 @@ -End-user CLI verification - -$ node --import tsx bin/tasks-axi.ts -v -0.2.4 -exit: 0 - -$ node --import tsx bin/tasks-axi.ts -V -0.2.4 -exit: 0 - -$ node --import tsx bin/tasks-axi.ts --version -0.2.4 -exit: 0 - -$ node --import tsx bin/tasks-axi.ts list --version -error: "Unknown flag: --version" -code: VALIDATION_ERROR -help[1]: Run the command with --help to see supported flags -exit: 2 - -Module-trace verification for the bare --version invocation - -present: src/version.ts -present: axi-sdk-js/dist/fast-path.js -absent: src/cli.ts -absent: @toon-format modules -absent: axi-sdk-js/dist/index.js - -The complete loader-produced trace is in version-module-trace.txt. diff --git a/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/version-module-trace.txt b/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/version-module-trace.txt deleted file mode 100644 index 1a85011..0000000 --- a/.no-mistakes/evidence/fm/tasksaxi-version-fastpath-adopt-p4/version-module-trace.txt +++ /dev/null @@ -1,3 +0,0 @@ -file:///Users/kunchen/.no-mistakes/worktrees/4ebfb4ced0b4/01KZD5GTT42WND2KE5SZK4D5Y5/bin/tasks-axi.ts -file:///Users/kunchen/.no-mistakes/worktrees/4ebfb4ced0b4/01KZD5GTT42WND2KE5SZK4D5Y5/node_modules/.pnpm/axi-sdk-js@0.1.10/node_modules/axi-sdk-js/dist/fast-path.js -file:///Users/kunchen/.no-mistakes/worktrees/4ebfb4ced0b4/01KZD5GTT42WND2KE5SZK4D5Y5/src/version.ts diff --git a/.no-mistakes/evidence/fm/tax-release-r7/bin-shebang.txt b/.no-mistakes/evidence/fm/tax-release-r7/bin-shebang.txt deleted file mode 100644 index 7c5ea96..0000000 --- a/.no-mistakes/evidence/fm/tax-release-r7/bin-shebang.txt +++ /dev/null @@ -1,2 +0,0 @@ -$ tar -xOf .no-mistakes/evidence/fm/tax-release-r7/tasks-axi-0.1.0.tgz package/dist/bin/tasks-axi.js | head -n 1 -#!/usr/bin/env node diff --git a/.no-mistakes/evidence/fm/tax-release-r7/excluded-dev-artifacts-check.txt b/.no-mistakes/evidence/fm/tax-release-r7/excluded-dev-artifacts-check.txt deleted file mode 100644 index b1ad4b0..0000000 --- a/.no-mistakes/evidence/fm/tax-release-r7/excluded-dev-artifacts-check.txt +++ /dev/null @@ -1,2 +0,0 @@ -$ tar -tzf .no-mistakes/evidence/fm/tax-release-r7/tasks-axi-0.1.0.tgz | grep -E "(\.d\.ts$|\.js\.map$|^package/src/|^package/test/)" -OK: tarball contains no .d.ts, .js.map, src/, or test/ entries diff --git a/.no-mistakes/evidence/fm/tax-release-r7/install-from-tarball-smoke.txt b/.no-mistakes/evidence/fm/tax-release-r7/install-from-tarball-smoke.txt deleted file mode 100644 index 3ab0298..0000000 --- a/.no-mistakes/evidence/fm/tax-release-r7/install-from-tarball-smoke.txt +++ /dev/null @@ -1,14 +0,0 @@ -$ mkdir -p .no-mistakes/tmp-release-smoke && cd .no-mistakes/tmp-release-smoke -$ npm init -y -$ npm install --no-audit --no-fund ../evidence/fm/tax-release-r7/tasks-axi-0.1.0.tgz - -added 3 packages in 410ms -$ ./node_modules/.bin/tasks-axi --version -0.1.0 -$ printf "# Backlog..." > backlog.md -$ TASKS_AXI_FILE=$PWD/backlog.md ./node_modules/.bin/tasks-axi list --state queued -count: 1 -tasks[1]{id,state,kind,repo,title}: - release-smoke,queued,task,"-",verify installed package runs -help[1]: - - Run `tasks-axi show ` for full notes on a task diff --git a/.no-mistakes/evidence/fm/tax-release-r7/npm-pack-json.txt b/.no-mistakes/evidence/fm/tax-release-r7/npm-pack-json.txt deleted file mode 100644 index bab5136..0000000 --- a/.no-mistakes/evidence/fm/tax-release-r7/npm-pack-json.txt +++ /dev/null @@ -1,170 +0,0 @@ -$ npm pack --pack-destination .no-mistakes/evidence/fm/tax-release-r7 --json - -> tasks-axi@0.1.0 prepack -> npm run build - - -> tasks-axi@0.1.0 build -> tsc - -[ - { - "id": "tasks-axi@0.1.0", - "name": "tasks-axi", - "version": "0.1.0", - "size": 35740, - "unpackedSize": 132347, - "shasum": "e2f43e05bbabffee8f04a7538c437f56bc291c19", - "integrity": "sha512-oORC/Fqt43QRr3QDLZUud27URS55YHyQTvKTaCw2nqLbErT5R349vHZD5blhSd6HmMLKzj3P95gIsYmrnJeSaA==", - "filename": "tasks-axi-0.1.0.tgz", - "files": [ - { - "path": "LICENSE", - "size": 1065, - "mode": 420 - }, - { - "path": "README.md", - "size": 8905, - "mode": 420 - }, - { - "path": "dist/bin/tasks-axi.js", - "size": 103, - "mode": 420 - }, - { - "path": "dist/src/args.js", - "size": 5719, - "mode": 420 - }, - { - "path": "dist/src/backends/lock.js", - "size": 4899, - "mode": 420 - }, - { - "path": "dist/src/backends/markdown-grammar.js", - "size": 11008, - "mode": 420 - }, - { - "path": "dist/src/backends/markdown.js", - "size": 24244, - "mode": 420 - }, - { - "path": "dist/src/body.js", - "size": 1857, - "mode": 420 - }, - { - "path": "dist/src/cli.js", - "size": 6228, - "mode": 420 - }, - { - "path": "dist/src/commands/crud.js", - "size": 15118, - "mode": 420 - }, - { - "path": "dist/src/commands/home.js", - "size": 2213, - "mode": 420 - }, - { - "path": "dist/src/commands/maintain.js", - "size": 2279, - "mode": 420 - }, - { - "path": "dist/src/commands/setup.js", - "size": 1338, - "mode": 420 - }, - { - "path": "dist/src/commands/state.js", - "size": 13192, - "mode": 420 - }, - { - "path": "dist/src/config.js", - "size": 6266, - "mode": 420 - }, - { - "path": "dist/src/context.js", - "size": 1026, - "mode": 420 - }, - { - "path": "dist/src/derive.js", - "size": 1775, - "mode": 420 - }, - { - "path": "dist/src/errors.js", - "size": 904, - "mode": 420 - }, - { - "path": "dist/src/fields.js", - "size": 1105, - "mode": 420 - }, - { - "path": "dist/src/format.js", - "size": 809, - "mode": 420 - }, - { - "path": "dist/src/id.js", - "size": 1934, - "mode": 420 - }, - { - "path": "dist/src/model.js", - "size": 416, - "mode": 420 - }, - { - "path": "dist/src/skill.js", - "size": 4607, - "mode": 420 - }, - { - "path": "dist/src/store.js", - "size": 44, - "mode": 420 - }, - { - "path": "dist/src/suggestions.js", - "size": 6023, - "mode": 420 - }, - { - "path": "dist/src/toon.js", - "size": 1681, - "mode": 420 - }, - { - "path": "dist/src/view.js", - "size": 2798, - "mode": 420 - }, - { - "path": "package.json", - "size": 1465, - "mode": 420 - }, - { - "path": "skills/tasks-axi/SKILL.md", - "size": 3326, - "mode": 420 - } - ], - "entryCount": 29, - "bundled": [] - } -] diff --git a/.no-mistakes/evidence/fm/tax-release-r7/npm-publish-dry-run.txt b/.no-mistakes/evidence/fm/tax-release-r7/npm-publish-dry-run.txt deleted file mode 100644 index 509d81b..0000000 --- a/.no-mistakes/evidence/fm/tax-release-r7/npm-publish-dry-run.txt +++ /dev/null @@ -1,53 +0,0 @@ -$ npm publish --dry-run --access public - -> tasks-axi@0.1.0 prepack -> npm run build - - -> tasks-axi@0.1.0 build -> tsc - -npm notice -npm notice 📦 tasks-axi@0.1.0 -npm notice Tarball Contents -npm notice 1.1kB LICENSE -npm notice 8.9kB README.md -npm notice 103B dist/bin/tasks-axi.js -npm notice 5.7kB dist/src/args.js -npm notice 4.9kB dist/src/backends/lock.js -npm notice 11.0kB dist/src/backends/markdown-grammar.js -npm notice 24.2kB dist/src/backends/markdown.js -npm notice 1.9kB dist/src/body.js -npm notice 6.2kB dist/src/cli.js -npm notice 15.1kB dist/src/commands/crud.js -npm notice 2.2kB dist/src/commands/home.js -npm notice 2.3kB dist/src/commands/maintain.js -npm notice 1.3kB dist/src/commands/setup.js -npm notice 13.2kB dist/src/commands/state.js -npm notice 6.3kB dist/src/config.js -npm notice 1.0kB dist/src/context.js -npm notice 1.8kB dist/src/derive.js -npm notice 904B dist/src/errors.js -npm notice 1.1kB dist/src/fields.js -npm notice 809B dist/src/format.js -npm notice 1.9kB dist/src/id.js -npm notice 416B dist/src/model.js -npm notice 4.6kB dist/src/skill.js -npm notice 44B dist/src/store.js -npm notice 6.0kB dist/src/suggestions.js -npm notice 1.7kB dist/src/toon.js -npm notice 2.8kB dist/src/view.js -npm notice 1.5kB package.json -npm notice 3.3kB skills/tasks-axi/SKILL.md -npm notice Tarball Details -npm notice name: tasks-axi -npm notice version: 0.1.0 -npm notice filename: tasks-axi-0.1.0.tgz -npm notice package size: 35.7 kB -npm notice unpacked size: 132.3 kB -npm notice shasum: e2f43e05bbabffee8f04a7538c437f56bc291c19 -npm notice integrity: sha512-oORC/Fqt43QRr[...]5gIsYmrnJeSaA== -npm notice total files: 29 -npm notice -npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access (dry-run) -+ tasks-axi@0.1.0 diff --git a/.no-mistakes/evidence/fm/tax-release-r7/packed-package-json.json b/.no-mistakes/evidence/fm/tax-release-r7/packed-package-json.json deleted file mode 100644 index d30209d..0000000 --- a/.no-mistakes/evidence/fm/tax-release-r7/packed-package-json.json +++ /dev/null @@ -1,65 +0,0 @@ -$ tar -xOf .no-mistakes/evidence/fm/tax-release-r7/tasks-axi-0.1.0.tgz package/package.json -{ - "name": "tasks-axi", - "version": "0.1.0", - "packageManager": "pnpm@11.1.1", - "description": "AXI-compliant task/backlog CLI — token-efficient TOON output, pluggable backends, byte-exact markdown round-trip, idempotent mutations", - "type": "module", - "repository": { - "type": "git", - "url": "git+https://github.com/kunchenguid/tasks-axi.git" - }, - "homepage": "https://github.com/kunchenguid/tasks-axi#readme", - "bugs": { - "url": "https://github.com/kunchenguid/tasks-axi/issues" - }, - "keywords": [ - "tasks", - "backlog", - "cli", - "agent", - "axi", - "toon", - "beads" - ], - "bin": { - "tasks-axi": "dist/bin/tasks-axi.js" - }, - "files": [ - "dist/**/*.js", - "skills/tasks-axi", - "LICENSE", - "README.md" - ], - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "tsc", - "build:skill": "tsx scripts/build-skill.ts", - "test": "vitest run", - "test:watch": "vitest", - "lint": "eslint .", - "dev": "tsx bin/tasks-axi.ts", - "prepack": "npm run build" - }, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "dependencies": { - "@toon-format/toon": "^2.1.0", - "axi-sdk-js": "^0.1.7" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@types/node": "^22.0.0", - "eslint": "^10.1.0", - "globals": "^17.4.0", - "prettier": "^3.8.1", - "tsx": "^4.0.0", - "typescript": "^5.7.0", - "typescript-eslint": "^8.58.0", - "vitest": "^3.0.0" - } -} diff --git a/.no-mistakes/evidence/fm/tax-release-r7/tarball-file-list.txt b/.no-mistakes/evidence/fm/tax-release-r7/tarball-file-list.txt deleted file mode 100644 index 3bb9c64..0000000 --- a/.no-mistakes/evidence/fm/tax-release-r7/tarball-file-list.txt +++ /dev/null @@ -1,30 +0,0 @@ -$ tar -tzf .no-mistakes/evidence/fm/tax-release-r7/tasks-axi-0.1.0.tgz | sort -package/LICENSE -package/README.md -package/dist/bin/tasks-axi.js -package/dist/src/args.js -package/dist/src/backends/lock.js -package/dist/src/backends/markdown-grammar.js -package/dist/src/backends/markdown.js -package/dist/src/body.js -package/dist/src/cli.js -package/dist/src/commands/crud.js -package/dist/src/commands/home.js -package/dist/src/commands/maintain.js -package/dist/src/commands/setup.js -package/dist/src/commands/state.js -package/dist/src/config.js -package/dist/src/context.js -package/dist/src/derive.js -package/dist/src/errors.js -package/dist/src/fields.js -package/dist/src/format.js -package/dist/src/id.js -package/dist/src/model.js -package/dist/src/skill.js -package/dist/src/store.js -package/dist/src/suggestions.js -package/dist/src/toon.js -package/dist/src/view.js -package/package.json -package/skills/tasks-axi/SKILL.md diff --git a/.no-mistakes/evidence/fm/tax-release-r7/tasks-axi-0.1.0.tgz b/.no-mistakes/evidence/fm/tax-release-r7/tasks-axi-0.1.0.tgz deleted file mode 100644 index dcad4474bf16d0dfe5d18fc672e166b2df1149cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35740 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 diff --git a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-done-readback.toon b/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-done-readback.toon deleted file mode 100644 index 105fa82..0000000 --- a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-done-readback.toon +++ /dev/null @@ -1,18 +0,0 @@ -task: - id: done-body-q1 - title: finishes from in-flight - state: done - blocked: no - blocked_by: none - held: no - hold_reason: "-" - hold_kind: "-" - hold_until: "-" - kind: task - repo: evidence - priority: "-" - created: "-" - closed: 2026-07-09 - deps: none - links: none - body: "First done paragraph.\n\nSecond done paragraph after a blank.\n## Intent\n\nPreserve this entire body on done." diff --git a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-move-source.md b/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-move-source.md deleted file mode 100644 index 169b234..0000000 --- a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-move-source.md +++ /dev/null @@ -1,10 +0,0 @@ -# Backlog - -## In flight - -## Queued - -- [ ] source-neighbor-q1 - remains in the source backlog (repo: evidence) - Neighbor body remains untouched. - -## Done diff --git a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-move-target.md b/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-move-target.md deleted file mode 100644 index 626f23c..0000000 --- a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-move-target.md +++ /dev/null @@ -1,16 +0,0 @@ -# Backlog - -## In flight - -## Queued - -- [ ] move-body-q1 - moves between backlogs (repo: evidence) - First move paragraph. - - Second move paragraph after a blank. - - ## Intent - - Preserve this entire body on mv. - -## Done diff --git a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-mv-readback.toon b/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-mv-readback.toon deleted file mode 100644 index c7f0355..0000000 --- a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-mv-readback.toon +++ /dev/null @@ -1,18 +0,0 @@ -task: - id: move-body-q1 - title: moves between backlogs - state: queued - blocked: no - blocked_by: none - held: no - hold_reason: "-" - hold_kind: "-" - hold_until: "-" - kind: task - repo: evidence - priority: "-" - created: "-" - closed: "-" - deps: none - links: none - body: "First move paragraph.\n\nSecond move paragraph after a blank.\n## Intent\n\nPreserve this entire body on mv." diff --git a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-start-readback.toon b/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-start-readback.toon deleted file mode 100644 index be355f4..0000000 --- a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-start-readback.toon +++ /dev/null @@ -1,18 +0,0 @@ -task: - id: start-body-q1 - title: starts from queued - state: in_flight - blocked: no - blocked_by: none - held: no - hold_reason: "-" - hold_kind: "-" - hold_until: "-" - kind: task - repo: evidence - priority: "-" - created: 2026-07-09 - closed: "-" - deps: none - links: none - body: "First start paragraph.\n\nSecond start paragraph after a blank.\n## Intent\n\nPreserve this entire body on start." diff --git a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-workflow.md b/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-workflow.md deleted file mode 100644 index 56b7a98..0000000 --- a/.no-mistakes/evidence/fm/taxi-body-blanks-b5/cli-workflow.md +++ /dev/null @@ -1,25 +0,0 @@ -# Backlog - -## In flight - -- [ ] start-body-q1 - starts from queued (repo: evidence) (since 2026-07-09) - First start paragraph. - - Second start paragraph after a blank. - - ## Intent - - Preserve this entire body on start. - -## Queued - -## Done - -- [x] done-body-q1 - finishes from in-flight (repo: evidence) (done 2026-07-09) - First done paragraph. - - Second done paragraph after a blank. - - ## Intent - - Preserve this entire body on done. diff --git a/.no-mistakes/evidence/fm/taxi-docs-d8/npm-pack-dry-run.txt b/.no-mistakes/evidence/fm/taxi-docs-d8/npm-pack-dry-run.txt deleted file mode 100644 index c3cd45f..0000000 --- a/.no-mistakes/evidence/fm/taxi-docs-d8/npm-pack-dry-run.txt +++ /dev/null @@ -1,51 +0,0 @@ - -> tasks-axi@0.1.0 prepack -> npm run build - - -> tasks-axi@0.1.0 build -> tsc - -npm notice -npm notice 📦 tasks-axi@0.1.0 -npm notice Tarball Contents -npm notice 1.1kB LICENSE -npm notice 9.3kB README.md -npm notice 103B dist/bin/tasks-axi.js -npm notice 5.7kB dist/src/args.js -npm notice 4.9kB dist/src/backends/lock.js -npm notice 11.2kB dist/src/backends/markdown-grammar.js -npm notice 24.2kB dist/src/backends/markdown.js -npm notice 1.9kB dist/src/body.js -npm notice 6.2kB dist/src/cli.js -npm notice 15.1kB dist/src/commands/crud.js -npm notice 2.2kB dist/src/commands/home.js -npm notice 2.3kB dist/src/commands/maintain.js -npm notice 1.3kB dist/src/commands/setup.js -npm notice 13.2kB dist/src/commands/state.js -npm notice 6.3kB dist/src/config.js -npm notice 1.0kB dist/src/context.js -npm notice 1.8kB dist/src/derive.js -npm notice 904B dist/src/errors.js -npm notice 1.1kB dist/src/fields.js -npm notice 809B dist/src/format.js -npm notice 1.9kB dist/src/id.js -npm notice 416B dist/src/model.js -npm notice 4.6kB dist/src/skill.js -npm notice 44B dist/src/store.js -npm notice 6.0kB dist/src/suggestions.js -npm notice 1.7kB dist/src/toon.js -npm notice 2.8kB dist/src/view.js -npm notice 1.5kB package.json -npm notice 3.3kB skills/tasks-axi/SKILL.md -npm notice Tarball Details -npm notice name: tasks-axi -npm notice version: 0.1.0 -npm notice filename: tasks-axi-0.1.0.tgz -npm notice package size: 35.9 kB -npm notice unpacked size: 132.9 kB -npm notice shasum: 2b1892442e49bda3b209784a0ddbf09ee99deaa8 -npm notice integrity: sha512-cPnpEgay6FUp0[...]hMgZUwYIwB7gw== -npm notice total files: 29 -npm notice -tasks-axi-0.1.0.tgz diff --git a/.no-mistakes/evidence/fm/taxi-docs-d8/update-body-file-notes.md b/.no-mistakes/evidence/fm/taxi-docs-d8/update-body-file-notes.md deleted file mode 100644 index 825a0bf..0000000 --- a/.no-mistakes/evidence/fm/taxi-docs-d8/update-body-file-notes.md +++ /dev/null @@ -1,2 +0,0 @@ -file replacement line one -file replacement line two diff --git a/.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md b/.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md deleted file mode 100644 index f65dd27..0000000 --- a/.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md +++ /dev/null @@ -1,9 +0,0 @@ -# Backlog - -## Queued -- [ ] nm-release-validation - clearer title (since 2026-06-23) - file replacement line one - file replacement line two - -## In flight -## Done diff --git a/.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-transcript.txt b/.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-transcript.txt deleted file mode 100644 index 32f46a3..0000000 --- a/.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-transcript.txt +++ /dev/null @@ -1,166 +0,0 @@ -Scratch backlog: .no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md -Scratch body file: .no-mistakes/evidence/fm/taxi-docs-d8/update-body-file-notes.md - -$ TASKS_AXI_FILE=.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md node dist/bin/tasks-axi.js update --help -usage: tasks-axi update [flags] -aliases: edit -flags: - --title , --body or --body-file , --append "" - --repo , --kind , --priority <0-4>, --pr , --report -examples: - tasks-axi update nm-release-validation --append "step 3 in progress on lavish #87" - tasks-axi update fm-x --repo firstmate --kind ship -$ TASKS_AXI_FILE=.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md node dist/bin/tasks-axi.js add nm-release-validation "original title" --body "initial note" -task: - id: nm-release-validation - title: original title - state: queued - blocked: no - blocked_by: none - kind: task - repo: "-" - priority: "-" - created: 2026-06-23 - closed: "-" - deps: none - links: none - body: initial note -help[2]: - - Run `tasks-axi start nm-release-validation` to move it to in flight - - Run `tasks-axi block nm-release-validation --by ` to record a dependency - -$ TASKS_AXI_FILE=.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md node dist/bin/tasks-axi.js update nm-release-validation --append "appended note" -task: - id: nm-release-validation - title: original title - state: queued - blocked: no - blocked_by: none - kind: task - repo: "-" - priority: "-" - created: 2026-06-23 - closed: "-" - deps: none - links: none - body: "initial note\nappended note" -help[1]: - - Run `tasks-axi show nm-release-validation --full` to see the result - -$ TASKS_AXI_FILE=.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md node dist/bin/tasks-axi.js show nm-release-validation --full -task: - id: nm-release-validation - title: original title - state: queued - blocked: no - blocked_by: none - kind: task - repo: "-" - priority: "-" - created: 2026-06-23 - closed: "-" - deps: none - links: none - body: "initial note\nappended note" - -$ TASKS_AXI_FILE=.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md node dist/bin/tasks-axi.js update nm-release-validation --body "replacement note" -task: - id: nm-release-validation - title: original title - state: queued - blocked: no - blocked_by: none - kind: task - repo: "-" - priority: "-" - created: 2026-06-23 - closed: "-" - deps: none - links: none - body: replacement note -help[1]: - - Run `tasks-axi show nm-release-validation --full` to see the result - -$ TASKS_AXI_FILE=.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md node dist/bin/tasks-axi.js show nm-release-validation --full -task: - id: nm-release-validation - title: original title - state: queued - blocked: no - blocked_by: none - kind: task - repo: "-" - priority: "-" - created: 2026-06-23 - closed: "-" - deps: none - links: none - body: replacement note - -$ TASKS_AXI_FILE=.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md node dist/bin/tasks-axi.js update nm-release-validation --body-file .no-mistakes/evidence/fm/taxi-docs-d8/update-body-file-notes.md -task: - id: nm-release-validation - title: original title - state: queued - blocked: no - blocked_by: none - kind: task - repo: "-" - priority: "-" - created: 2026-06-23 - closed: "-" - deps: none - links: none - body: "file replacement line one\nfile replacement line two\n" -help[1]: - - Run `tasks-axi show nm-release-validation --full` to see the result - -$ TASKS_AXI_FILE=.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md node dist/bin/tasks-axi.js update nm-release-validation --title "clearer title" -task: - id: nm-release-validation - title: clearer title - state: queued - blocked: no - blocked_by: none - kind: task - repo: "-" - priority: "-" - created: 2026-06-23 - closed: "-" - deps: none - links: none - body: "file replacement line one\nfile replacement line two\n" -help[1]: - - Run `tasks-axi show nm-release-validation --full` to see the result - -$ TASKS_AXI_FILE=.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md node dist/bin/tasks-axi.js show nm-release-validation --full -task: - id: nm-release-validation - title: clearer title - state: queued - blocked: no - blocked_by: none - kind: task - repo: "-" - priority: "-" - created: 2026-06-23 - closed: "-" - deps: none - links: none - body: "file replacement line one\nfile replacement line two\n" - -$ TASKS_AXI_FILE=.no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md node dist/bin/tasks-axi.js update nm-release-validation --body inline --body-file .no-mistakes/evidence/fm/taxi-docs-d8/update-body-file-notes.md -error: Use only one of --body or --body-file -code: VALIDATION_ERROR -exit_status: 2 - -$ sed -n 1,80p .no-mistakes/evidence/fm/taxi-docs-d8/update-e2e-backlog.md -# Backlog - -## Queued -- [ ] nm-release-validation - clearer title (since 2026-06-23) - file replacement line one - file replacement line two - -## In flight -## Done diff --git a/.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md b/.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md deleted file mode 100644 index 05a5798..0000000 --- a/.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md +++ /dev/null @@ -1,10 +0,0 @@ -# Backlog - -## In flight - -## Queued -- [ ] add-tests-q7 - two lines now (repo: app) blocked-by: fix-login-k3 - waits on the login refactor - -## Done -- [x] fix-login-k3 - one line (repo: app, since 2026-06-20) (done 2026-06-23) -- [x] legacy-done-z1 - one line - https://github.com/o/r/pull/12 (merged 2026-06-21) diff --git a/.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-transcript.txt b/.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-transcript.txt deleted file mode 100644 index 42dd8f9..0000000 --- a/.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-transcript.txt +++ /dev/null @@ -1,87 +0,0 @@ -# tasks-axi firstmate markdown interop CLI transcript -This drives the real CLI against a firstmate-shaped backlog file. - -$ sed -n "1,120p" .no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md -# Backlog - -## In flight -- [ ] fix-login-k3 - one line (repo: app, since 2026-06-20) - -## Queued -- [ ] add-tests-q7 - one line (repo: app) blocked-by: fix-login-k3 - waits on the login refactor - -## Done -- [x] legacy-done-z1 - one line - https://github.com/o/r/pull/12 (merged 2026-06-21) - -$ pnpm exec tsx bin/tasks-axi.ts list --file .no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md --fields blocked,blocked_by,deps -count: 3 -tasks[3]{id,state,kind,repo,title,blocked,blocked_by,deps}: - fix-login-k3,in_flight,task,"app, since 2026-06-20",one line,no,none,none - add-tests-q7,queued,task,app,one line,yes,fix-login-k3,"blocked-by:fix-login-k3" - legacy-done-z1,done,task,"-","one line - https://github.com/o/r/pull/12",no,none,none -help[2]: - - Run `tasks-axi show --file=.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md` for full notes on a task - - Run `tasks-axi ready --file=.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md` to see unblocked queued work - -$ pnpm exec tsx bin/tasks-axi.ts ready --file .no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md -count: 0 -ready: 0 unblocked queued tasks -help[1]: - - Run `tasks-axi list --state queued --file=.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md` to see all queued work (incl. blocked) - -$ pnpm exec tsx bin/tasks-axi.ts update add-tests-q7 --file .no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md --title "two lines now" -task: - id: add-tests-q7 - title: two lines now - state: queued - blocked: yes - blocked_by: fix-login-k3 - kind: task - repo: app - priority: "-" - created: "-" - closed: "-" - deps: "blocked-by:fix-login-k3" - links: none - body: "" -help[1]: - - Run `tasks-axi show add-tests-q7 --full --file=.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md` to see the result - -$ sed -n "1,120p" .no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md -# Backlog - -## In flight -- [ ] fix-login-k3 - one line (repo: app, since 2026-06-20) - -## Queued -- [ ] add-tests-q7 - two lines now (repo: app) blocked-by: fix-login-k3 - waits on the login refactor - -## Done -- [x] legacy-done-z1 - one line - https://github.com/o/r/pull/12 (merged 2026-06-21) - -$ pnpm exec tsx bin/tasks-axi.ts done fix-login-k3 --file .no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md --no-prune -done: - id: fix-login-k3 - state: done - pruned: 0 -help[1]: - - Run `tasks-axi ready --file=.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md` to dispatch work unblocked by this - -$ pnpm exec tsx bin/tasks-axi.ts ready --file .no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md -count: 1 -ready[1]{id,state,kind,repo,title}: - add-tests-q7,queued,task,app,two lines now -help[1]: - - Run `tasks-axi start --file=.no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md` to dispatch one of these - -$ sed -n "1,160p" .no-mistakes/evidence/fm/taxi-fmt-f9/firstmate-cli-backlog.md -# Backlog - -## In flight - -## Queued -- [ ] add-tests-q7 - two lines now (repo: app) blocked-by: fix-login-k3 - waits on the login refactor - -## Done -- [x] fix-login-k3 - one line (repo: app, since 2026-06-20) (done 2026-06-23) -- [x] legacy-done-z1 - one line - https://github.com/o/r/pull/12 (merged 2026-06-21) diff --git a/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/cli-transcript.md b/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/cli-transcript.md deleted file mode 100644 index bbba1cf..0000000 --- a/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/cli-transcript.md +++ /dev/null @@ -1,34 +0,0 @@ -# Linked-set `mv` CLI evidence - -The successful command moved a blocker and its dependent in one call. - -```console -$ pnpm exec tsx bin/tasks-axi.ts mv blocker-b1 dependent-d2 --to .no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-destination-after.md --file .no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-source-after.md --json -{ - "ok": true, - "action": "mv", - "ids": [ - "blocker-b1", - "dependent-d2" - ], - "from": "/Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KX58SG95JCMJPFPXFXVY317M/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-source-after.md", - "to": "/Users/kunchen/.no-mistakes/worktrees/6a0c69bae187/01KX58SG95JCMJPFPXFXVY317M/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-destination-after.md" -} -``` - -The resulting [destination backlog](success-destination-after.md) contains both tasks, the blocker body, and the exact `blocked-by` reason. - -The resulting [source backlog](success-source-after.md) contains neither moved task. - -Moving only the dependent is rejected before either backlog is changed. - -```console -$ pnpm exec tsx bin/tasks-axi.ts mv dependent-d2 --to .no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/refusal-destination-after.md --file .no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/refusal-source-after.md -error: "Cannot move \"dependent-d2\": its blocker \"blocker-b1\" would be stranded (not in the moved set and absent from the destination)" -code: VALIDATION_ERROR -help[1]: "Add \"blocker-b1\" to the same `mv`, or move it to the destination first" -``` - -The command exited with code 2. - -The unchanged [refusal source](refusal-source-after.md) still has the linked pair and the [refusal destination](refusal-destination-after.md) is still empty. diff --git a/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/refusal-destination-after.md b/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/refusal-destination-after.md deleted file mode 100644 index a3d5e56..0000000 --- a/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/refusal-destination-after.md +++ /dev/null @@ -1,7 +0,0 @@ -# Destination backlog - -## In flight - -## Queued - -## Done diff --git a/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/refusal-source-after.md b/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/refusal-source-after.md deleted file mode 100644 index 923b772..0000000 --- a/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/refusal-source-after.md +++ /dev/null @@ -1,10 +0,0 @@ -# Source backlog - -## In flight - -## Queued - -- [ ] blocker-b1 - refactor login (repo: alpha) -- [ ] dependent-d2 - migrate auth (repo: alpha) blocked-by: blocker-b1 - waits on login refactor - -## Done diff --git a/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-destination-after.md b/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-destination-after.md deleted file mode 100644 index a3bf673..0000000 --- a/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-destination-after.md +++ /dev/null @@ -1,19 +0,0 @@ -# Destination backlog - -## In flight - -## Queued - -- [ ] blocker-b1 - refactor login (repo: alpha) - First paragraph from the blocker. - - Second paragraph remains part of its task body. - - ## Intent - - Preserve this indented heading as task body text. - -- [ ] dependent-d2 - migrate auth (repo: alpha) blocked-by: blocker-b1 - waits on login refactor - Dependent body survives with its link reason. - -## Done diff --git a/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-source-after.md b/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-source-after.md deleted file mode 100644 index 4cdae11..0000000 --- a/.no-mistakes/evidence/fm/taxi-mv-linked-sets-m3/success-source-after.md +++ /dev/null @@ -1,7 +0,0 @@ -# Source backlog - -## In flight - -## Queued - -## Done From cb97c320f118c81dd1128bda7de86d0d03cd4e4d Mon Sep 17 00:00:00 2001 From: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:43:48 -0700 Subject: [PATCH 05/10] chore: gitignore no-mistakes evidence dir (contributor safety) (#41) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3bdd52e..e9be04b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ dist/ .DS_Store +.no-mistakes/evidence/ From 48a5a903b150f29644e5f9a6a2869b90d0fb5b2b Mon Sep 17 00:00:00 2001 From: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:15:11 -0700 Subject: [PATCH 06/10] chore(agents): use @AGENTS.md import instead of CLAUDE.md symlink (#42) Co-authored-by: Kun Chen --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) mode change 120000 => 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 47dc3e3..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a9d4d26 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,2 @@ + +@AGENTS.md From 7b9291ffc5d8afc70f5e7210b361dd30d7f8f00c Mon Sep 17 00:00:00 2001 From: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:21:12 -0700 Subject: [PATCH 07/10] ci: require no-mistakes pipeline attestation in the gate (#45) * ci: require no-mistakes pipeline attestation in the gate * test: run the no-mistakes gate script on POSIX legs only --- .github/workflows/no-mistakes-required.yml | 192 ++++++++++++++++-- AGENTS.md | 1 + test/workflows/no-mistakes-gate.test.ts | 218 +++++++++++++++++++++ 3 files changed, 395 insertions(+), 16 deletions(-) create mode 100644 test/workflows/no-mistakes-gate.test.ts diff --git a/.github/workflows/no-mistakes-required.yml b/.github/workflows/no-mistakes-required.yml index afcaf18..9837e54 100644 --- a/.github/workflows/no-mistakes-required.yml +++ b/.github/workflows/no-mistakes-required.yml @@ -34,7 +34,7 @@ jobs: github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'release-please[bot]' steps: - - name: Verify no-mistakes signature in PR body + - name: Verify no-mistakes signature and pipeline attestation in PR body env: PR_BODY: ${{ github.event.pull_request.body }} PR_AUTHOR: ${{ github.event.pull_request.user.login }} @@ -44,19 +44,179 @@ jobs: marker='Updates from [git push no-mistakes](https://github.com/kunchenguid/no-mistakes)' if printf '%s' "${PR_BODY:-}" | grep -qF -- "$marker"; then echo "Found no-mistakes signature in PR #${PR_NUMBER} body." - exit 0 + else + { + echo "::error::This PR was not raised through no-mistakes." + echo + echo "Contributions to this repository must be submitted via 'git push no-mistakes'." + echo "That pipeline runs the required review/test/lint/CI steps and writes a" + echo "deterministic '## Pipeline' section into the PR body containing:" + echo + echo " $marker" + echo + echo "See CONTRIBUTING.md for setup and the full workflow." + echo + echo "PR author: ${PR_AUTHOR}" + } >&2 + exit 1 fi - { - echo "::error::This PR was not raised through no-mistakes." - echo - echo "Contributions to this repository must be submitted via 'git push no-mistakes'." - echo "That pipeline runs the required review/test/lint/CI steps and writes a" - echo "deterministic '## Pipeline' section into the PR body containing:" - echo - echo " $marker" - echo - echo "See CONTRIBUTING.md for setup and the full workflow." - echo - echo "PR author: ${PR_AUTHOR}" - } >&2 - exit 1 + + # The signature alone only proves the pipeline wrote the body. no-mistakes + # >= 1.46.0 also emits a machine-readable step attestation next to it; parse + # that to prove review, test, and document actually ran to completion. + # Contract: docs/reference/pipeline-steps.md#pipeline-step-attestation in + # kunchenguid/no-mistakes. + attestation_prefix='' + + if ! printf '%s' "${PR_BODY:-}" | grep -qF -- "$attestation_prefix"; then + echo "::error::This PR carries the no-mistakes signature but no pipeline attestation." + { + echo + echo "no-mistakes >= 1.46.0 is required (PR 670). That release writes a" + echo "machine-readable comment next to the signature:" + echo + echo " ${attestation_prefix}{\"head_sha\":\"...\",\"steps\":[...]}${attestation_suffix}" + echo + echo "Upgrade no-mistakes ('no-mistakes update'), then re-run" + echo "'git push no-mistakes' so the PR body is rewritten with the attestation." + echo + echo "PR author: ${PR_AUTHOR}" + } >&2 + exit 1 + fi + + # Exact-substring extraction (no regex): first prefix, then the first + # closing token after it, mirroring how no-mistakes' own consumer test + # slices the payload. + payload="$( + printf '%s' "${PR_BODY:-}" | tr -d '\r' | awk -v pre="$attestation_prefix" -v suf="$attestation_suffix" ' + found { next } + { + p = index($0, pre) + if (p == 0) next + rest = substr($0, p + length(pre)) + s = index(rest, suf) + if (s == 0) next + print substr(rest, 1, s - 1) + found = 1 + } + ' + )" + + if [ -z "$payload" ]; then + echo "::error::The no-mistakes pipeline attestation comment is malformed: no JSON payload could be extracted." + { + echo + echo "The '${attestation_prefix}' marker is present but is not closed by" + echo "'${attestation_suffix}' on the same line, or the payload is empty." + echo "This gate fails closed on an unreadable attestation." + echo + echo "PR author: ${PR_AUTHOR}" + } >&2 + exit 1 + fi + + # Real JSON parsing. Emits one "\t\t" line per + # required step. A step recorded more than once must be completed in + # every record. Any skip-shaped sibling key (a future 'skipped', + # 'skip_reason', 'quota_...', '..._unavailable' field) with a meaningful + # value is rejected outright, so a skip can never ride along on a + # 'completed' status. + if ! report="$( + printf '%s' "$payload" | jq -r --argjson required '["review","test","document"]' ' + if type != "object" then error("attestation payload is not a JSON object") else . end + | if (.steps | type) != "array" then error("attestation payload has no \"steps\" array") else . end + | . as $attestation + | $required[] + | . as $name + | [ $attestation.steps[] | select((type == "object") and ((.step? | tostring) == $name)) ] as $records + | if ($records | length) == 0 then + "\($name)\tmissing\tno record in attestation" + else + ([ $records[] | (.status? // null) | tostring ] | unique) as $statuses + | ([ $records[] + | to_entries[] + | select((.key | ascii_downcase) | test("skip|quota|unavailable")) + | select(.value != null and .value != false and .value != "") + | .key ] | unique) as $skip_markers + | if ($skip_markers | length) > 0 then + "\($name)\tskip-marker\t\($skip_markers | join(","))" + elif $statuses == ["completed"] then + "\($name)\tok\tcompleted" + else + "\($name)\tbad\t\($statuses | join(","))" + end + end + ' 2>&1 + )"; then + echo "::error::The no-mistakes pipeline attestation payload could not be parsed as JSON." + { + echo + echo "jq reported:" + echo "$report" + echo + echo "Payload: $payload" + echo + echo "This gate fails closed on an unparseable attestation." + echo + echo "PR author: ${PR_AUTHOR}" + } >&2 + exit 1 + fi + + echo "Attestation head_sha: $(printf '%s' "$payload" | jq -r '.head_sha // "(absent)"')" + + tab="$(printf '\t')" + gate_status=0 + checked=0 + while IFS="$tab" read -r name verdict detail; do + [ -n "$name" ] || continue + checked=$((checked + 1)) + case "$verdict" in + ok) + echo " ok: ${name} = completed" + ;; + missing) + echo "::error::The no-mistakes pipeline attestation has no '${name}' step record; '${name}' must be recorded as completed." + gate_status=1 + ;; + skip-marker) + echo "::error::The no-mistakes pipeline attestation marks '${name}' with skip indicator(s) [${detail}]; quota-exhaustion and agent-unavailability skips are not accepted." + gate_status=1 + ;; + *) + echo "::error::The no-mistakes pipeline attestation records '${name}' as '${detail}', not 'completed'." + gate_status=1 + ;; + esac + done <&2 + exit 1 + fi + + echo "no-mistakes pipeline attestation verified for PR #${PR_NUMBER}: review, test, and document all completed." diff --git a/AGENTS.md b/AGENTS.md index 30ac55e..9790c06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,6 +89,7 @@ Any argv shape other than exactly one version flag falls through to `runAxiCli`, In a fresh clone, run `pnpm install --frozen-lockfile` before manual pack or publish. Verify with `npm pack --dry-run` (no source/test cruft; bin is `dist/bin/tasks-axi.js` with its shebang preserved by tsc). - **CI is a 3-OS matrix** (ubuntu/macos/windows) running install → build → lint → test → `build:skill --check`. The `Require no-mistakes` and `Guard generated files` checks gate every PR to `main`. +- **The `Require no-mistakes` gate is mirrored verbatim from gh-axi.** Its inline `run:` script (signature + `no-mistakes-pipeline-attestation:v1` comment, requiring `review`/`test`/`document` all `completed`, failing closed on skip markers or malformed JSON) is copied byte-for-byte across the `*-axi` siblings; only this repo's `on:`/`if:`/`concurrency` are local. `test/workflows/no-mistakes-gate.test.ts` extracts that exact block from the YAML and executes it against fixtures, so edit the workflow, never a copy of the script. That test runs on the POSIX matrix legs only - Windows `jq` emits CRLF, which the ubuntu-only gate script has no reason to tolerate. ## Follow-ups (out of P1 scope) diff --git a/test/workflows/no-mistakes-gate.test.ts b/test/workflows/no-mistakes-gate.test.ts new file mode 100644 index 0000000..bd6608e --- /dev/null +++ b/test/workflows/no-mistakes-gate.test.ts @@ -0,0 +1,218 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; + +const root = fileURLToPath(new URL("../..", import.meta.url)); +const workflowPath = join( + root, + ".github", + "workflows", + "no-mistakes-required.yml", +); + +/** + * The gate lives as an inline `run:` block so the whole file can be mirrored + * into sibling repositories as one unit. Extract that exact block and execute + * it, so these tests exercise what CI runs rather than a copy of it. + */ +function extractGateScript(): string { + const doc = parse(readFileSync(workflowPath, "utf8")) as { + jobs: { check: { steps: Array<{ run?: string }> } }; + }; + const script = doc.jobs.check.steps[0]?.run; + if (!script) throw new Error("no-mistakes gate step has no run block"); + return script; +} + +const scriptPath = join(mkdtempSync(join(tmpdir(), "nm-gate-")), "gate.sh"); +writeFileSync(scriptPath, extractGateScript()); + +function hasCommand(command: string): boolean { + return spawnSync("sh", ["-c", `command -v ${command}`]).status === 0; +} + +// The gate is a bash script that parses JSON with jq, exactly as the +// ubuntu-latest runner this workflow pins does - and only there. The Windows +// leg of this repo's test matrix is deliberately excluded: its jq writes CRLF +// line endings, so every verdict the script reads back would carry a trailing +// \r and fail on an environment the gate never actually runs in. The gate +// script must stay byte-identical to the sibling repos', so the platform is +// filtered here rather than the script being made CRLF-tolerant. +const posix = process.platform !== "win32"; +const runnable = posix && hasCommand("bash") && hasCommand("jq"); + +// Never skip on the POSIX CI legs: a silently skipped gate test is worse than +// no test. Locally, skip when bash or jq is absent rather than failing a +// contributor's `pnpm test` over an unrelated missing tool. +if (process.env.CI && posix && !runnable) { + throw new Error( + "CI must provide bash and jq to exercise the no-mistakes gate", + ); +} + +function runGate(body: string): { code: number; output: string } { + const result = spawnSync("bash", [scriptPath], { + env: { + ...process.env, + PR_BODY: body, + PR_AUTHOR: "somedev", + PR_NUMBER: "42", + }, + encoding: "utf8", + }); + return { + code: result.status ?? -1, + output: `${result.stdout}${result.stderr}`, + }; +} + +const SIGNATURE = + "Updates from [git push no-mistakes](https://github.com/kunchenguid/no-mistakes)"; +const ATTESTATION_PREFIX = ""; +const HEAD_SHA = "12df13109c6ad8d64646b85ac7170b23afe6e9bf"; + +/** A PR body shaped like the one no-mistakes writes. */ +function prBody(attestationPayload?: string): string { + const attestation = + attestationPayload === undefined + ? "" + : `${ATTESTATION_PREFIX}${attestationPayload}${ATTESTATION_SUFFIX}\n\n`; + return [ + "## What Changed\n\n- something\n", + `## Pipeline\n\n${SIGNATURE}\n\n${attestation}`, + "
\nReview\n\nok\n\n
\n", + ].join("\n"); +} + +function attestation(steps: Array<[string, string]>): string { + return JSON.stringify({ + head_sha: HEAD_SHA, + steps: steps.map(([step, status]) => ({ step, status })), + }); +} + +/** The step snapshot a healthy run produces when the PR body is written. */ +const HEALTHY_STEPS: Array<[string, string]> = [ + ["intent", "completed"], + ["rebase", "completed"], + ["review", "completed"], + ["test", "completed"], + ["document", "completed"], + ["lint", "completed"], + ["push", "completed"], + ["pr", "running"], + ["ci", "pending"], +]; + +function withStatus(step: string, status: string): Array<[string, string]> { + return HEALTHY_STEPS.map(([name, current]) => + name === step ? [name, status] : [name, current], + ) as Array<[string, string]>; +} + +describe.runIf(runnable)("no-mistakes PR gate", () => { + it("accepts a body whose attestation completes review, test, and document", () => { + const { code, output } = runGate(prBody(attestation(HEALTHY_STEPS))); + expect(code).toBe(0); + expect(output).toContain("review, test, and document all completed"); + }); + + it("still rejects a body with no no-mistakes signature", () => { + const { code, output } = runGate("## Intent\n\nhand-written body\n"); + expect(code).toBe(1); + expect(output).toContain("was not raised through no-mistakes"); + expect(output).toContain("git push no-mistakes"); + }); + + it("rejects a signed body with no attestation and names the required version", () => { + const { code, output } = runGate(prBody()); + expect(code).toBe(1); + expect(output).toContain("no pipeline attestation"); + expect(output).toContain("no-mistakes >= 1.46.0 is required (PR 670)"); + }); + + // Every skip route no-mistakes has - `--skip`, a user skip at a gate, an + // automatic pipeline skip, or a run that ran out of agent quota - lands on + // the raw `skipped` status, and an unavailable agent surfaces as `failed`. + for (const status of ["skipped", "failed", "running", "pending"]) { + it(`rejects an attestation whose test step is ${status}`, () => { + const { code, output } = runGate( + prBody(attestation(withStatus("test", status))), + ); + expect(code).toBe(1); + expect(output).toContain(`records 'test' as '${status}'`); + }); + } + + it("rejects an attestation that omits a required step entirely", () => { + const steps = HEALTHY_STEPS.filter(([name]) => name !== "document"); + const { code, output } = runGate(prBody(attestation(steps))); + expect(code).toBe(1); + expect(output).toContain("no 'document' step record"); + }); + + it("rejects a required step recorded twice unless every record completed", () => { + const steps: Array<[string, string]> = [ + ...HEALTHY_STEPS, + ["review", "skipped"], + ]; + const { code, output } = runGate(prBody(attestation(steps))); + expect(code).toBe(1); + expect(output).toContain("records 'review' as 'completed,skipped'"); + }); + + // v1 carries no skip sibling field, so `status` is the only skip channel + // today. Fail closed if a later schema ever hangs a skip reason off an + // otherwise-completed step instead of widening the gate silently. + for (const marker of [ + { skip_reason: "quota exhausted" }, + { skipped: true }, + { agent_unavailable: true }, + { quota_exhausted: true }, + ]) { + const key = Object.keys(marker)[0]; + it(`rejects a completed step carrying a ${key} marker`, () => { + const payload = JSON.stringify({ + head_sha: HEAD_SHA, + steps: HEALTHY_STEPS.map(([step, status]) => + step === "review" ? { step, status, ...marker } : { step, status }, + ), + }); + const { code, output } = runGate(prBody(payload)); + expect(code).toBe(1); + expect(output).toContain(`skip indicator(s) [${key}]`); + }); + } + + it("fails closed on an attestation payload that is not valid JSON", () => { + const { code, output } = runGate( + prBody('{"head_sha":"abc","steps":[{"step":"review",'), + ); + expect(code).toBe(1); + expect(output).toContain("could not be parsed as JSON"); + }); + + it("fails closed when the payload has no steps array", () => { + const { code, output } = runGate(prBody('{"head_sha":"abc"}')); + expect(code).toBe(1); + expect(output).toContain("could not be parsed as JSON"); + }); + + it("fails closed when the attestation comment is never closed", () => { + const body = `## Pipeline\n\n${SIGNATURE}\n\n${ATTESTATION_PREFIX}{"head_sha":"abc","steps":[]}\n`; + const { code, output } = runGate(body); + expect(code).toBe(1); + expect(output).toContain("no JSON payload could be extracted"); + }); + + it("accepts a CRLF body", () => { + const body = prBody(attestation(HEALTHY_STEPS)).replace(/\n/g, "\r\n"); + const { code } = runGate(body); + expect(code).toBe(0); + }); +}); From 24ddd22db217a844f2103f4ea78247b9be86819b Mon Sep 17 00:00:00 2001 From: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Date: Thu, 20 Aug 2026 19:39:16 -0700 Subject: [PATCH 08/10] ci: bind the no-mistakes attestation to the current PR head (#46) --- .github/workflows/no-mistakes-required.yml | 27 ++++++++++- test/workflows/no-mistakes-gate.test.ts | 56 ++++++++++++++++++++-- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/.github/workflows/no-mistakes-required.yml b/.github/workflows/no-mistakes-required.yml index 9837e54..4a11477 100644 --- a/.github/workflows/no-mistakes-required.yml +++ b/.github/workflows/no-mistakes-required.yml @@ -39,6 +39,7 @@ jobs: PR_BODY: ${{ github.event.pull_request.body }} PR_AUTHOR: ${{ github.event.pull_request.user.login }} PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set -eu marker='Updates from [git push no-mistakes](https://github.com/kunchenguid/no-mistakes)' @@ -165,7 +166,31 @@ jobs: exit 1 fi - echo "Attestation head_sha: $(printf '%s' "$payload" | jq -r '.head_sha // "(absent)"')" + attested_head="$(printf '%s' "$payload" | jq -r '.head_sha // ""')" + echo "Attestation head_sha: ${attested_head:-(absent)}" + + # Head binding. The attestation describes the commit no-mistakes ran + # its steps on; a later push moves the PR head without rewriting the + # body, so an attestation that does not name the current head proves + # nothing about the code being merged. A `synchronize` whose body was + # NOT rewritten by no-mistakes going red is the intended contract, not + # a false positive. + if [ -z "$attested_head" ] || [ -z "${PR_HEAD_SHA:-}" ] || [ "$attested_head" != "$PR_HEAD_SHA" ]; then + echo "::error::The no-mistakes pipeline attestation is STALE for the current head of PR #${PR_NUMBER}." + { + echo + echo "Attestation head_sha: ${attested_head:-(absent)}" + echo "PR head sha: ${PR_HEAD_SHA:-(absent)}" + echo + echo "A commit was pushed after the no-mistakes run, so the attestation does not" + echo "describe the code this PR now proposes to merge." + echo + echo "Re-run 'git push no-mistakes' to refresh it." + echo + echo "PR author: ${PR_AUTHOR}" + } >&2 + exit 1 + fi tab="$(printf '\t')" gate_status=0 diff --git a/test/workflows/no-mistakes-gate.test.ts b/test/workflows/no-mistakes-gate.test.ts index bd6608e..0d29e86 100644 --- a/test/workflows/no-mistakes-gate.test.ts +++ b/test/workflows/no-mistakes-gate.test.ts @@ -54,13 +54,17 @@ if (process.env.CI && posix && !runnable) { ); } -function runGate(body: string): { code: number; output: string } { +function runGate( + body: string, + headSha: string = HEAD_SHA, +): { code: number; output: string } { const result = spawnSync("bash", [scriptPath], { env: { ...process.env, PR_BODY: body, PR_AUTHOR: "somedev", PR_NUMBER: "42", + PR_HEAD_SHA: headSha, }, encoding: "utf8", }); @@ -89,9 +93,12 @@ function prBody(attestationPayload?: string): string { ].join("\n"); } -function attestation(steps: Array<[string, string]>): string { +function attestation( + steps: Array<[string, string]>, + headSha: string = HEAD_SHA, +): string { return JSON.stringify({ - head_sha: HEAD_SHA, + head_sha: headSha, steps: steps.map(([step, status]) => ({ step, status })), }); } @@ -189,6 +196,49 @@ describe.runIf(runnable)("no-mistakes PR gate", () => { }); } + // Head binding: the attestation describes the commit no-mistakes ran on, so + // an attestation naming any other commit says nothing about what is being + // merged. A synchronize whose body was not rewritten by no-mistakes going red + // is the contract, not a false positive. + it("accepts an attestation whose head_sha is the PR's current head", () => { + const { code, output } = runGate( + prBody(attestation(HEALTHY_STEPS, HEAD_SHA)), + HEAD_SHA, + ); + expect(code).toBe(0); + expect(output).toContain(`Attestation head_sha: ${HEAD_SHA}`); + }); + + it("rejects an attestation whose head_sha is not the PR's current head", () => { + const staleSha = "0000000000000000000000000000000000000000"; + const { code, output } = runGate( + prBody(attestation(HEALTHY_STEPS, staleSha)), + HEAD_SHA, + ); + expect(code).toBe(1); + expect(output).toContain("attestation is STALE for the current head"); + expect(output).toContain("Re-run 'git push no-mistakes' to refresh it"); + expect(output).toContain(staleSha); + expect(output).toContain(HEAD_SHA); + }); + + it("fails closed when the attestation carries no head_sha at all", () => { + const payload = JSON.stringify({ + steps: HEALTHY_STEPS.map(([step, status]) => ({ step, status })), + }); + const { code, output } = runGate(prBody(payload), HEAD_SHA); + expect(code).toBe(1); + expect(output).toContain("attestation is STALE for the current head"); + expect(output).toContain("Attestation head_sha: (absent)"); + }); + + it("fails closed when the PR head sha is unavailable", () => { + const { code, output } = runGate(prBody(attestation(HEALTHY_STEPS)), ""); + expect(code).toBe(1); + expect(output).toContain("attestation is STALE for the current head"); + expect(output).toMatch(/PR head sha: +\(absent\)/); + }); + it("fails closed on an attestation payload that is not valid JSON", () => { const { code, output } = runGate( prBody('{"head_sha":"abc","steps":[{"step":"review",'), From 4c09455dae9ee3635093697834cb316e9150835f Mon Sep 17 00:00:00 2001 From: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:29:22 -0700 Subject: [PATCH 09/10] ci: migrate the no-mistakes gate to the shared composite action (#47) Replace the inline gate `run:` block in .github/workflows/no-mistakes-required.yml with a thin caller of kunchenguid/no-mistakes/.github/actions/require-no-mistakes, pinned to an immutable commit SHA. Enforcement logic and its tests now live upstream, so this repository no longer carries a hand-copied script that can drift from its siblings. Drop `synchronize` from the pull_request trigger: the verdict is a pure function of the PR body, and the pipeline pushes before it writes the Pipeline section, so a push-triggered run pinned a failure to a head whose body the same run was about to fix. This repo's ruleset is advisory with no required status check, so dropping the trigger cannot wedge a merge. Remove test/workflows/no-mistakes-gate.test.ts, which extracted and executed the now-absent inline block, and point AGENTS.md at the shared action. --- .github/workflows/no-mistakes-required.yml | 240 +++--------------- AGENTS.md | 3 +- test/workflows/no-mistakes-gate.test.ts | 268 --------------------- 3 files changed, 30 insertions(+), 481 deletions(-) delete mode 100644 test/workflows/no-mistakes-gate.test.ts diff --git a/.github/workflows/no-mistakes-required.yml b/.github/workflows/no-mistakes-required.yml index 4a11477..3e6ab32 100644 --- a/.github/workflows/no-mistakes-required.yml +++ b/.github/workflows/no-mistakes-required.yml @@ -3,7 +3,14 @@ run-name: "PR #${{ github.event.pull_request.number }} body compliance - ${{ git on: pull_request: - types: [opened, edited, synchronize, reopened] + # The verdict is a pure function of pull_request.body, so a push carries no + # new body to judge but does move the head SHA. The pipeline pushes before + # it writes the deterministic Pipeline section, so a synchronize trigger + # pinned a FAILURE check run to the new head for a body the same run was + # about to fix. GitHub keeps that failure next to the later edited SUCCESS, + # and gh pr checks collapses same-named check runs by startedAt alone, so the + # CI monitor could park the run red forever (PR #773). + types: [opened, edited, reopened] branches: - main # Never create a run for a release-please PR. The job-level author exemption @@ -20,7 +27,7 @@ permissions: # GitHub concurrency groups retain at most one pending run, replacing older # pending runs even when cancel-in-progress is false. Give body-bearing events # an immutable per-event group so first-time-fork approvals can never collapse -# opened/edited checks. Keep synchronize/reopened coalescing as before. +# opened/edited checks. Keep reopened coalescing as before. concurrency: group: no-mistakes-required-${{ github.event.pull_request.number }}-${{ (github.event.action == 'opened' || github.event.action == 'edited') && github.run_id || 'head-change' }} cancel-in-progress: true @@ -29,219 +36,28 @@ jobs: check: name: PR must be raised via no-mistakes runs-on: ubuntu-latest + # Known automation accounts are exempt so automation keeps working: + # - github-actions[bot] opens PRs via GITHUB_TOKEN + # - dependabot[bot] opens dependency update PRs + # - release-please[bot] opens release PRs + # Other authors (human or bot) must raise PRs through `git push no-mistakes`. + # + # These stay job-level rather than moving to the action's `exempt-authors` + # input on purpose: an in-job exemption still requires the run to start, and + # a GITHUB_TOKEN PR's run is created in action_required and never starts. + # Keeping the condition here preserves the exact verdict shape this + # repository's gate already produces for those authors. if: >- github.event.pull_request.user.login != 'github-actions[bot]' && github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'release-please[bot]' steps: + # The enforcement itself now lives in the shared composite action in the + # no-mistakes repository, so this repository no longer carries its own + # copy of the script to drift. + # + # Pinned to an immutable commit, never @main: main is editable by the very + # pull request this gate is judging. Bumping the pin is a separate, + # deliberate pull request. - name: Verify no-mistakes signature and pipeline attestation in PR body - env: - PR_BODY: ${{ github.event.pull_request.body }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - set -eu - marker='Updates from [git push no-mistakes](https://github.com/kunchenguid/no-mistakes)' - if printf '%s' "${PR_BODY:-}" | grep -qF -- "$marker"; then - echo "Found no-mistakes signature in PR #${PR_NUMBER} body." - else - { - echo "::error::This PR was not raised through no-mistakes." - echo - echo "Contributions to this repository must be submitted via 'git push no-mistakes'." - echo "That pipeline runs the required review/test/lint/CI steps and writes a" - echo "deterministic '## Pipeline' section into the PR body containing:" - echo - echo " $marker" - echo - echo "See CONTRIBUTING.md for setup and the full workflow." - echo - echo "PR author: ${PR_AUTHOR}" - } >&2 - exit 1 - fi - - # The signature alone only proves the pipeline wrote the body. no-mistakes - # >= 1.46.0 also emits a machine-readable step attestation next to it; parse - # that to prove review, test, and document actually ran to completion. - # Contract: docs/reference/pipeline-steps.md#pipeline-step-attestation in - # kunchenguid/no-mistakes. - attestation_prefix='' - - if ! printf '%s' "${PR_BODY:-}" | grep -qF -- "$attestation_prefix"; then - echo "::error::This PR carries the no-mistakes signature but no pipeline attestation." - { - echo - echo "no-mistakes >= 1.46.0 is required (PR 670). That release writes a" - echo "machine-readable comment next to the signature:" - echo - echo " ${attestation_prefix}{\"head_sha\":\"...\",\"steps\":[...]}${attestation_suffix}" - echo - echo "Upgrade no-mistakes ('no-mistakes update'), then re-run" - echo "'git push no-mistakes' so the PR body is rewritten with the attestation." - echo - echo "PR author: ${PR_AUTHOR}" - } >&2 - exit 1 - fi - - # Exact-substring extraction (no regex): first prefix, then the first - # closing token after it, mirroring how no-mistakes' own consumer test - # slices the payload. - payload="$( - printf '%s' "${PR_BODY:-}" | tr -d '\r' | awk -v pre="$attestation_prefix" -v suf="$attestation_suffix" ' - found { next } - { - p = index($0, pre) - if (p == 0) next - rest = substr($0, p + length(pre)) - s = index(rest, suf) - if (s == 0) next - print substr(rest, 1, s - 1) - found = 1 - } - ' - )" - - if [ -z "$payload" ]; then - echo "::error::The no-mistakes pipeline attestation comment is malformed: no JSON payload could be extracted." - { - echo - echo "The '${attestation_prefix}' marker is present but is not closed by" - echo "'${attestation_suffix}' on the same line, or the payload is empty." - echo "This gate fails closed on an unreadable attestation." - echo - echo "PR author: ${PR_AUTHOR}" - } >&2 - exit 1 - fi - - # Real JSON parsing. Emits one "\t\t" line per - # required step. A step recorded more than once must be completed in - # every record. Any skip-shaped sibling key (a future 'skipped', - # 'skip_reason', 'quota_...', '..._unavailable' field) with a meaningful - # value is rejected outright, so a skip can never ride along on a - # 'completed' status. - if ! report="$( - printf '%s' "$payload" | jq -r --argjson required '["review","test","document"]' ' - if type != "object" then error("attestation payload is not a JSON object") else . end - | if (.steps | type) != "array" then error("attestation payload has no \"steps\" array") else . end - | . as $attestation - | $required[] - | . as $name - | [ $attestation.steps[] | select((type == "object") and ((.step? | tostring) == $name)) ] as $records - | if ($records | length) == 0 then - "\($name)\tmissing\tno record in attestation" - else - ([ $records[] | (.status? // null) | tostring ] | unique) as $statuses - | ([ $records[] - | to_entries[] - | select((.key | ascii_downcase) | test("skip|quota|unavailable")) - | select(.value != null and .value != false and .value != "") - | .key ] | unique) as $skip_markers - | if ($skip_markers | length) > 0 then - "\($name)\tskip-marker\t\($skip_markers | join(","))" - elif $statuses == ["completed"] then - "\($name)\tok\tcompleted" - else - "\($name)\tbad\t\($statuses | join(","))" - end - end - ' 2>&1 - )"; then - echo "::error::The no-mistakes pipeline attestation payload could not be parsed as JSON." - { - echo - echo "jq reported:" - echo "$report" - echo - echo "Payload: $payload" - echo - echo "This gate fails closed on an unparseable attestation." - echo - echo "PR author: ${PR_AUTHOR}" - } >&2 - exit 1 - fi - - attested_head="$(printf '%s' "$payload" | jq -r '.head_sha // ""')" - echo "Attestation head_sha: ${attested_head:-(absent)}" - - # Head binding. The attestation describes the commit no-mistakes ran - # its steps on; a later push moves the PR head without rewriting the - # body, so an attestation that does not name the current head proves - # nothing about the code being merged. A `synchronize` whose body was - # NOT rewritten by no-mistakes going red is the intended contract, not - # a false positive. - if [ -z "$attested_head" ] || [ -z "${PR_HEAD_SHA:-}" ] || [ "$attested_head" != "$PR_HEAD_SHA" ]; then - echo "::error::The no-mistakes pipeline attestation is STALE for the current head of PR #${PR_NUMBER}." - { - echo - echo "Attestation head_sha: ${attested_head:-(absent)}" - echo "PR head sha: ${PR_HEAD_SHA:-(absent)}" - echo - echo "A commit was pushed after the no-mistakes run, so the attestation does not" - echo "describe the code this PR now proposes to merge." - echo - echo "Re-run 'git push no-mistakes' to refresh it." - echo - echo "PR author: ${PR_AUTHOR}" - } >&2 - exit 1 - fi - - tab="$(printf '\t')" - gate_status=0 - checked=0 - while IFS="$tab" read -r name verdict detail; do - [ -n "$name" ] || continue - checked=$((checked + 1)) - case "$verdict" in - ok) - echo " ok: ${name} = completed" - ;; - missing) - echo "::error::The no-mistakes pipeline attestation has no '${name}' step record; '${name}' must be recorded as completed." - gate_status=1 - ;; - skip-marker) - echo "::error::The no-mistakes pipeline attestation marks '${name}' with skip indicator(s) [${detail}]; quota-exhaustion and agent-unavailability skips are not accepted." - gate_status=1 - ;; - *) - echo "::error::The no-mistakes pipeline attestation records '${name}' as '${detail}', not 'completed'." - gate_status=1 - ;; - esac - done <&2 - exit 1 - fi - - echo "no-mistakes pipeline attestation verified for PR #${PR_NUMBER}: review, test, and document all completed." + uses: kunchenguid/no-mistakes/.github/actions/require-no-mistakes@32d396ac0f29135daf7fcb9964aba9d5f4e796d6 # post-v1.57.1, untagged (action added in #819) diff --git a/AGENTS.md b/AGENTS.md index 9790c06..c2cee96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,7 +89,8 @@ Any argv shape other than exactly one version flag falls through to `runAxiCli`, In a fresh clone, run `pnpm install --frozen-lockfile` before manual pack or publish. Verify with `npm pack --dry-run` (no source/test cruft; bin is `dist/bin/tasks-axi.js` with its shebang preserved by tsc). - **CI is a 3-OS matrix** (ubuntu/macos/windows) running install → build → lint → test → `build:skill --check`. The `Require no-mistakes` and `Guard generated files` checks gate every PR to `main`. -- **The `Require no-mistakes` gate is mirrored verbatim from gh-axi.** Its inline `run:` script (signature + `no-mistakes-pipeline-attestation:v1` comment, requiring `review`/`test`/`document` all `completed`, failing closed on skip markers or malformed JSON) is copied byte-for-byte across the `*-axi` siblings; only this repo's `on:`/`if:`/`concurrency` are local. `test/workflows/no-mistakes-gate.test.ts` extracts that exact block from the YAML and executes it against fixtures, so edit the workflow, never a copy of the script. That test runs on the POSIX matrix legs only - Windows `jq` emits CRLF, which the ubuntu-only gate script has no reason to tolerate. +- **The `Require no-mistakes` gate is a thin caller of a shared composite action.** `.github/workflows/no-mistakes-required.yml` delegates enforcement to `kunchenguid/no-mistakes/.github/actions/require-no-mistakes`, pinned to an immutable commit SHA and never `@main` (main is editable by the very PR the gate judges). Enforcement logic and its tests live upstream in the no-mistakes repo - change enforcement there rather than hand-copying a script between siblings, and bump this repo's pin in a deliberate separate PR. This repo still owns its `on:`, `paths-ignore`, `concurrency`, `permissions`, job name, and author-exemption `if:`. +- The shared action binds the attestation to the PR's current head, so a PR whose body no-mistakes did not rewrite for that head goes red. That is the attestation contract, not a flake: push through `git push no-mistakes` so the body is refreshed. `on.pull_request.types` deliberately omits `synchronize` - the verdict is a pure function of the PR body, and a push-triggered run pins a failure to a head whose body the same pipeline run is about to fix. ## Follow-ups (out of P1 scope) diff --git a/test/workflows/no-mistakes-gate.test.ts b/test/workflows/no-mistakes-gate.test.ts deleted file mode 100644 index 0d29e86..0000000 --- a/test/workflows/no-mistakes-gate.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; -import { parse } from "yaml"; - -const root = fileURLToPath(new URL("../..", import.meta.url)); -const workflowPath = join( - root, - ".github", - "workflows", - "no-mistakes-required.yml", -); - -/** - * The gate lives as an inline `run:` block so the whole file can be mirrored - * into sibling repositories as one unit. Extract that exact block and execute - * it, so these tests exercise what CI runs rather than a copy of it. - */ -function extractGateScript(): string { - const doc = parse(readFileSync(workflowPath, "utf8")) as { - jobs: { check: { steps: Array<{ run?: string }> } }; - }; - const script = doc.jobs.check.steps[0]?.run; - if (!script) throw new Error("no-mistakes gate step has no run block"); - return script; -} - -const scriptPath = join(mkdtempSync(join(tmpdir(), "nm-gate-")), "gate.sh"); -writeFileSync(scriptPath, extractGateScript()); - -function hasCommand(command: string): boolean { - return spawnSync("sh", ["-c", `command -v ${command}`]).status === 0; -} - -// The gate is a bash script that parses JSON with jq, exactly as the -// ubuntu-latest runner this workflow pins does - and only there. The Windows -// leg of this repo's test matrix is deliberately excluded: its jq writes CRLF -// line endings, so every verdict the script reads back would carry a trailing -// \r and fail on an environment the gate never actually runs in. The gate -// script must stay byte-identical to the sibling repos', so the platform is -// filtered here rather than the script being made CRLF-tolerant. -const posix = process.platform !== "win32"; -const runnable = posix && hasCommand("bash") && hasCommand("jq"); - -// Never skip on the POSIX CI legs: a silently skipped gate test is worse than -// no test. Locally, skip when bash or jq is absent rather than failing a -// contributor's `pnpm test` over an unrelated missing tool. -if (process.env.CI && posix && !runnable) { - throw new Error( - "CI must provide bash and jq to exercise the no-mistakes gate", - ); -} - -function runGate( - body: string, - headSha: string = HEAD_SHA, -): { code: number; output: string } { - const result = spawnSync("bash", [scriptPath], { - env: { - ...process.env, - PR_BODY: body, - PR_AUTHOR: "somedev", - PR_NUMBER: "42", - PR_HEAD_SHA: headSha, - }, - encoding: "utf8", - }); - return { - code: result.status ?? -1, - output: `${result.stdout}${result.stderr}`, - }; -} - -const SIGNATURE = - "Updates from [git push no-mistakes](https://github.com/kunchenguid/no-mistakes)"; -const ATTESTATION_PREFIX = ""; -const HEAD_SHA = "12df13109c6ad8d64646b85ac7170b23afe6e9bf"; - -/** A PR body shaped like the one no-mistakes writes. */ -function prBody(attestationPayload?: string): string { - const attestation = - attestationPayload === undefined - ? "" - : `${ATTESTATION_PREFIX}${attestationPayload}${ATTESTATION_SUFFIX}\n\n`; - return [ - "## What Changed\n\n- something\n", - `## Pipeline\n\n${SIGNATURE}\n\n${attestation}`, - "
\nReview\n\nok\n\n
\n", - ].join("\n"); -} - -function attestation( - steps: Array<[string, string]>, - headSha: string = HEAD_SHA, -): string { - return JSON.stringify({ - head_sha: headSha, - steps: steps.map(([step, status]) => ({ step, status })), - }); -} - -/** The step snapshot a healthy run produces when the PR body is written. */ -const HEALTHY_STEPS: Array<[string, string]> = [ - ["intent", "completed"], - ["rebase", "completed"], - ["review", "completed"], - ["test", "completed"], - ["document", "completed"], - ["lint", "completed"], - ["push", "completed"], - ["pr", "running"], - ["ci", "pending"], -]; - -function withStatus(step: string, status: string): Array<[string, string]> { - return HEALTHY_STEPS.map(([name, current]) => - name === step ? [name, status] : [name, current], - ) as Array<[string, string]>; -} - -describe.runIf(runnable)("no-mistakes PR gate", () => { - it("accepts a body whose attestation completes review, test, and document", () => { - const { code, output } = runGate(prBody(attestation(HEALTHY_STEPS))); - expect(code).toBe(0); - expect(output).toContain("review, test, and document all completed"); - }); - - it("still rejects a body with no no-mistakes signature", () => { - const { code, output } = runGate("## Intent\n\nhand-written body\n"); - expect(code).toBe(1); - expect(output).toContain("was not raised through no-mistakes"); - expect(output).toContain("git push no-mistakes"); - }); - - it("rejects a signed body with no attestation and names the required version", () => { - const { code, output } = runGate(prBody()); - expect(code).toBe(1); - expect(output).toContain("no pipeline attestation"); - expect(output).toContain("no-mistakes >= 1.46.0 is required (PR 670)"); - }); - - // Every skip route no-mistakes has - `--skip`, a user skip at a gate, an - // automatic pipeline skip, or a run that ran out of agent quota - lands on - // the raw `skipped` status, and an unavailable agent surfaces as `failed`. - for (const status of ["skipped", "failed", "running", "pending"]) { - it(`rejects an attestation whose test step is ${status}`, () => { - const { code, output } = runGate( - prBody(attestation(withStatus("test", status))), - ); - expect(code).toBe(1); - expect(output).toContain(`records 'test' as '${status}'`); - }); - } - - it("rejects an attestation that omits a required step entirely", () => { - const steps = HEALTHY_STEPS.filter(([name]) => name !== "document"); - const { code, output } = runGate(prBody(attestation(steps))); - expect(code).toBe(1); - expect(output).toContain("no 'document' step record"); - }); - - it("rejects a required step recorded twice unless every record completed", () => { - const steps: Array<[string, string]> = [ - ...HEALTHY_STEPS, - ["review", "skipped"], - ]; - const { code, output } = runGate(prBody(attestation(steps))); - expect(code).toBe(1); - expect(output).toContain("records 'review' as 'completed,skipped'"); - }); - - // v1 carries no skip sibling field, so `status` is the only skip channel - // today. Fail closed if a later schema ever hangs a skip reason off an - // otherwise-completed step instead of widening the gate silently. - for (const marker of [ - { skip_reason: "quota exhausted" }, - { skipped: true }, - { agent_unavailable: true }, - { quota_exhausted: true }, - ]) { - const key = Object.keys(marker)[0]; - it(`rejects a completed step carrying a ${key} marker`, () => { - const payload = JSON.stringify({ - head_sha: HEAD_SHA, - steps: HEALTHY_STEPS.map(([step, status]) => - step === "review" ? { step, status, ...marker } : { step, status }, - ), - }); - const { code, output } = runGate(prBody(payload)); - expect(code).toBe(1); - expect(output).toContain(`skip indicator(s) [${key}]`); - }); - } - - // Head binding: the attestation describes the commit no-mistakes ran on, so - // an attestation naming any other commit says nothing about what is being - // merged. A synchronize whose body was not rewritten by no-mistakes going red - // is the contract, not a false positive. - it("accepts an attestation whose head_sha is the PR's current head", () => { - const { code, output } = runGate( - prBody(attestation(HEALTHY_STEPS, HEAD_SHA)), - HEAD_SHA, - ); - expect(code).toBe(0); - expect(output).toContain(`Attestation head_sha: ${HEAD_SHA}`); - }); - - it("rejects an attestation whose head_sha is not the PR's current head", () => { - const staleSha = "0000000000000000000000000000000000000000"; - const { code, output } = runGate( - prBody(attestation(HEALTHY_STEPS, staleSha)), - HEAD_SHA, - ); - expect(code).toBe(1); - expect(output).toContain("attestation is STALE for the current head"); - expect(output).toContain("Re-run 'git push no-mistakes' to refresh it"); - expect(output).toContain(staleSha); - expect(output).toContain(HEAD_SHA); - }); - - it("fails closed when the attestation carries no head_sha at all", () => { - const payload = JSON.stringify({ - steps: HEALTHY_STEPS.map(([step, status]) => ({ step, status })), - }); - const { code, output } = runGate(prBody(payload), HEAD_SHA); - expect(code).toBe(1); - expect(output).toContain("attestation is STALE for the current head"); - expect(output).toContain("Attestation head_sha: (absent)"); - }); - - it("fails closed when the PR head sha is unavailable", () => { - const { code, output } = runGate(prBody(attestation(HEALTHY_STEPS)), ""); - expect(code).toBe(1); - expect(output).toContain("attestation is STALE for the current head"); - expect(output).toMatch(/PR head sha: +\(absent\)/); - }); - - it("fails closed on an attestation payload that is not valid JSON", () => { - const { code, output } = runGate( - prBody('{"head_sha":"abc","steps":[{"step":"review",'), - ); - expect(code).toBe(1); - expect(output).toContain("could not be parsed as JSON"); - }); - - it("fails closed when the payload has no steps array", () => { - const { code, output } = runGate(prBody('{"head_sha":"abc"}')); - expect(code).toBe(1); - expect(output).toContain("could not be parsed as JSON"); - }); - - it("fails closed when the attestation comment is never closed", () => { - const body = `## Pipeline\n\n${SIGNATURE}\n\n${ATTESTATION_PREFIX}{"head_sha":"abc","steps":[]}\n`; - const { code, output } = runGate(body); - expect(code).toBe(1); - expect(output).toContain("no JSON payload could be extracted"); - }); - - it("accepts a CRLF body", () => { - const body = prBody(attestation(HEALTHY_STEPS)).replace(/\n/g, "\r\n"); - const { code } = runGate(body); - expect(code).toBe(0); - }); -}); From d9175b6d083d693c5b6ca21652454d52e4b312d9 Mon Sep 17 00:00:00 2001 From: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:57:12 -0700 Subject: [PATCH 10/10] fix: make the generated skill defer to live CLI guidance (#48) * fix(skill): shrink SKILL.md to a CLI-deferring stub Installed skills go stale when the npm package is bumped. Keep only identity frontmatter plus pointers to live CLI help so regeneration cannot re-inflate baked command docs. Co-authored-by: Cursor * no-mistakes(review): Align skill documentation with minimal generator contract * no-mistakes(document): Consolidate generated skill documentation --------- Co-authored-by: Cursor --- AGENTS.md | 6 ++-- CONTRIBUTING.md | 2 +- README.md | 2 +- scripts/build-skill.ts | 4 +-- skills/tasks-axi/SKILL.md | 44 +++---------------------- src/skill.ts | 67 ++++++++------------------------------- test/skill.test.ts | 33 ++++++++++--------- 7 files changed, 41 insertions(+), 117 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c2cee96..50e311d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ The CLI layer never knows which backend is active — it only talks to the `Stor - `src/backends/markdown*.ts` — the only P1 backend. - `src/public-followup.ts` - authoritative versioned schema, strict privacy-safe validation, canonical encoding, immutable-field checks, relation/event readiness, and terminal-state invariants for `kind=public-followup`; `src/commands/public-followup.ts` owns its dedicated CLI state machine. - `src/commands/*` — one file per verb group; `src/view.ts` owns the read-side TOON projection; `src/confirm.ts` owns the write-side output (the `ok:` confirmation line, the `--json` payload, and `renderMutation`, which assembles both). -- Shared helpers copied from the family: `args.ts`, `body.ts`, `format.ts`, `fields.ts`, `toon.ts`, `suggestions.ts`, `skill.ts`. +- Shared helpers copied from the family: `args.ts`, `body.ts`, `format.ts`, `fields.ts`, `toon.ts`, `suggestions.ts`, `skill.ts` (minimal CLI-deferring stub generator). ## Markdown grammar invariants (the hard part — do not regress) @@ -76,8 +76,8 @@ Any argv shape other than exactly one version flag falls through to `runAxiCli`, ## Build / test / ship -- `pnpm build` (tsc), `pnpm test` (vitest, `test/` mirrors `src/`), `pnpm lint` (eslint), `pnpm run build:skill -- --check` (the generated `skills/tasks-axi/SKILL.md` is built from `DESCRIPTION` + `TOP_HELP` and must not drift — CI runs the check). -- `skills/tasks-axi/SKILL.md` is generated — regenerate with `pnpm run build:skill` after changing the description or top-level help; never hand-edit it. +- `pnpm build` (tsc), `pnpm test` (vitest, `test/` mirrors `src/`), `pnpm lint` (eslint), `pnpm run build:skill -- --check` (CI fails if `skills/tasks-axi/SKILL.md` drifts from `src/skill.ts`). +- The shipped skill stays **minimal** and **defers to the CLI** for all actual guidance. Frontmatter (name/description/metadata) is the discovery surface; the body only says what tasks-axi is, when to reach for it, and pointers to `npx -y tasks-axi` (dashboard), `npx -y tasks-axi --help`, and `npx -y tasks-axi --help`. tasks-axi CLI output is the single source of truth. Never re-duplicate CLI-owned commands, flags, or workflow steps into the skill - prefer a pointer. Never hand-edit `skills/tasks-axi/SKILL.md`; regenerate with `pnpm run build:skill`. - This repo is no-mistakes-gated; ship through `/no-mistakes`. ### Release & packaging (mirrors the `*-axi` siblings) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index baccbe5..8abb021 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,7 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star - Run the full gate before pushing: `pnpm build && pnpm lint && pnpm test && pnpm run build:skill -- --check`. - The CLI layer only talks to the `Store` interface; backends slot in behind it without touching command code. - Do not hand-edit `CHANGELOG.md` or `.release-please-manifest.json` - release-please owns them. -- Do not hand-edit `skills/tasks-axi/SKILL.md` - it is generated from the CLI's own description and help by `pnpm run build:skill`. Regenerate and commit it after changing the description or top-level help; CI fails if it is stale. +- Do not hand-edit `skills/tasks-axi/SKILL.md` - it is generated from `src/skill.ts`. After changing the generator or shared description, run `pnpm run build:skill` and commit the result; CI fails if it is stale. ## Release and Packaging diff --git a/README.md b/README.md index a9221b3..9de1d65 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ pnpm lint # eslint pnpm run build:skill -- --check # fail if the generated skill is stale ``` -The installable skill is generated from the same description and help the CLI prints, so it can never drift. +The generated installable skill is intentionally minimal and points agents to the live CLI for all commands, flags, and workflows. CLI output remains the single source of truth. ## Contributing diff --git a/scripts/build-skill.ts b/scripts/build-skill.ts index 0a97fef..f74064b 100644 --- a/scripts/build-skill.ts +++ b/scripts/build-skill.ts @@ -1,5 +1,5 @@ -// Generates skills/tasks-axi/SKILL.md from the shared CLI guidance so the -// installable skill never drifts from what `tasks-axi` prints. +// Generates skills/tasks-axi/SKILL.md as a minimal stub that defers to the +// live CLI for all actual guidance (see src/skill.ts). // // pnpm run build:skill # write the file // pnpm run build:skill -- --check # fail (exit 1) if the committed file is stale diff --git a/skills/tasks-axi/SKILL.md b/skills/tasks-axi/SKILL.md index 6cec190..6846716 100644 --- a/skills/tasks-axi/SKILL.md +++ b/skills/tasks-axi/SKILL.md @@ -13,48 +13,14 @@ metadata: Agent ergonomic task & backlog manager for the current workspace. Prefer this over hand-editing backlog.md for task state, dependency, or hold changes. -You do not need tasks-axi installed globally - invoke it with `npx -y tasks-axi `. -If tasks-axi output shows a follow-up command starting with `tasks-axi`, run it as `npx -y tasks-axi ...` instead. - -tasks-axi operates on a hand-editable `backlog.md` in the current workspace (or the path set in `.tasks.toml`). It edits the file in place with a byte-exact round-trip, so the human-readable backlog stays the source of truth. - ## When to use Use tasks-axi whenever a task touches the backlog: filing or dispatching work, moving a task through queued -> in flight -> done, recording a PR url or report path on completion, tracking blocked-by dependencies, pausing dispatch with structured holds, finding dispatchable ready work or intentionally held work, or trimming the Done list. -## Workflow - -1. Run `npx -y tasks-axi` with no arguments for a dashboard of the current backlog - in flight work, queued work with blockers, and suggested next commands. -2. Drill in verb-first: `list`, `show `, `ready`, then mutate with `add`, `start`, `done`, `block`/`unblock`, `hold`/`unhold`, `update`. -3. The long notes never appear in `list`; run `show --full` to read a task's complete body before replacing it. -4. `add` takes a caller-supplied id (the join key), e.g. `tasks-axi add fm-x "title" --kind ship --repo firstmate --start`; or pass `--mint` to generate a slug-xx id from the title. -5. `done --pr ` (or `--report `) closes a task, records the link, and prunes the Done list (archived, never deleted). Then `ready` shows work it unblocked. -6. `hold --reason ""` pauses dispatch without prose parsing; `ready` excludes active holds by default, and `ready --include-held` shows a separate held group. - Use `--until YYYY-MM-DD` for a date gate that becomes inactive on and after that date. -7. Human-readable responses include contextual next-step hints under `help:` when there is a useful follow-up - follow them. -8. `--json` mutation responses skip `help:` and return the deterministic result object instead. - -## Commands - -``` -commands[19]: - (none)=dashboard, add, list, show, start, done, reopen, update, rm, block, unblock, hold, unhold, ready, public-followup, mv, prune, render, setup -``` - -Run `npx -y tasks-axi --help` for global flags, or `npx -y tasks-axi --help` for per-command usage. +Get every command, flag, and workflow from the live CLI - it is the single source of truth: -## Tips +- `npx -y tasks-axi` - dashboard of the current backlog +- `npx -y tasks-axi --help` - global usage +- `npx -y tasks-axi --help` - per-command usage -- Output is TOON-encoded and token-efficient; the long task body is truncated by default - the whole point is that `list` stays cheap. - Use `--full` only when you need the complete notes. -- Every write leads with an `ok:` line confirming the write result, including the resulting task state when the command changes one (e.g. `ok: start -> In flight`, `ok: done -> Done (pr )`, `ok: render -> normalized `), then state-aware next-step hints. - Mutations are idempotent and add `already: true` on a no-op; re-running is safe. -- Pass `--json` to any mutation (`add`, `start`, `done`, `reopen`, `update`, `rm`, `block`, `unblock`, `hold`, `unhold`, `mv`, `prune`, `render`) for a machine-readable result object (`{ "ok": true, "action": ..., "task": { ... } }` or operation-specific result fields) instead of TOON - confirm a write deterministically without a follow-up read. -- `block --by ` and `unblock` manage the dependency graph; `hold --reason "" [--until YYYY-MM-DD]` and `unhold` manage structured dispatch pauses; `ready` lists only queued work with no unresolved blocker and no active hold. -- Filter `list` with `--state`, `--repo`, `--kind`, `--blocked`, `--limit`, and add columns with `--fields a,b,c`. - Use `list --state held` or `--fields held,hold_reason,hold_kind,hold_until` when scanning active hold state. -- Existing prose markers such as `HELD`, `PARKED`, `DEFERRED`, `CAPTAIN-DECISION`, and `do not dispatch` stay prose until intentionally migrated. - Preserve the original prose as the hold reason, then choose `captain`, `parked`, `future`, `load`, or `external` only when the text supports that bucket. -- Note writes are inspect-then-update: run `show --full`, then replace the curated current body with `update --body ""` or `--body-file `. - Add `--archive-body` to preserve the superseded body in `note-archive.md`; `--title ""` replaces the title; `render` normalizes the file; `mv [...] --to ` moves one or more tasks to another backlog in one atomic transaction - pass a whole connected set (a blocker and its dependents) to move it together and preserve its `blocked-by` links and reason strings; moves that would strand an endpoint are refused. -- Free-form (no-id) backlog lines are preserved verbatim and are never modified. +You do not need tasks-axi installed globally. If the CLI prints a follow-up starting with `tasks-axi`, run it as `npx -y tasks-axi ...` instead. diff --git a/src/skill.ts b/src/skill.ts index 69745a7..70b1608 100644 --- a/src/skill.ts +++ b/src/skill.ts @@ -1,4 +1,4 @@ -import { DESCRIPTION, TOP_HELP } from "./cli.js"; +import { DESCRIPTION } from "./cli.js"; // Trigger string agents match against to auto-load the skill. Terse and // outcome-focused so it fires on "manage the backlog / track tasks" intents. @@ -21,21 +21,13 @@ function yamlDoubleQuote(value: string): string { } /** - * Extract the `commands[N]:` block from the top-level help so the skill's - * command list can never drift from what `tasks-axi --help` prints. - */ -export function extractCommandsBlock(): string { - const match = TOP_HELP.match(/^(commands\[\d+\]:\n(?: {2}.*\n)+)/m); - if (!match) { - throw new Error("Could not find commands block in TOP_HELP"); - } - return match[1].trimEnd(); -} - -/** - * Render the installable SKILL.md. The body is built from the same shared - * guidance the CLI prints (description + top-level help), rewriting invocations - * to non-interactive `npx -y tasks-axi ...` so the CLI comes along on demand. + * Render the installable SKILL.md as a minimal stub. + * + * Frontmatter is the skill's identity and discovery surface. The body only + * says what tasks-axi is, when to reach for it, and where to get live + * instructions: the CLI itself. Never bake CLI-owned commands, flags, or + * workflow steps here - an installed skill goes stale when the npm package + * is bumped, and `pnpm run build:skill` would re-inflate any such copy. */ export function createSkillMarkdown(): string { return `--- @@ -53,49 +45,16 @@ metadata: ${DESCRIPTION} -You do not need tasks-axi installed globally - invoke it with \`npx -y tasks-axi \`. -If tasks-axi output shows a follow-up command starting with \`tasks-axi\`, run it as \`npx -y tasks-axi ...\` instead. - -tasks-axi operates on a hand-editable \`backlog.md\` in the current workspace (or the path set in \`.tasks.toml\`). It edits the file in place with a byte-exact round-trip, so the human-readable backlog stays the source of truth. - ## When to use Use tasks-axi whenever a task touches the backlog: filing or dispatching work, moving a task through queued -> in flight -> done, recording a PR url or report path on completion, tracking blocked-by dependencies, pausing dispatch with structured holds, finding dispatchable ready work or intentionally held work, or trimming the Done list. -## Workflow - -1. Run \`npx -y tasks-axi\` with no arguments for a dashboard of the current backlog - in flight work, queued work with blockers, and suggested next commands. -2. Drill in verb-first: \`list\`, \`show \`, \`ready\`, then mutate with \`add\`, \`start\`, \`done\`, \`block\`/\`unblock\`, \`hold\`/\`unhold\`, \`update\`. -3. The long notes never appear in \`list\`; run \`show --full\` to read a task's complete body before replacing it. -4. \`add\` takes a caller-supplied id (the join key), e.g. \`tasks-axi add fm-x "title" --kind ship --repo firstmate --start\`; or pass \`--mint\` to generate a slug-xx id from the title. -5. \`done --pr \` (or \`--report \`) closes a task, records the link, and prunes the Done list (archived, never deleted). Then \`ready\` shows work it unblocked. -6. \`hold --reason ""\` pauses dispatch without prose parsing; \`ready\` excludes active holds by default, and \`ready --include-held\` shows a separate held group. - Use \`--until YYYY-MM-DD\` for a date gate that becomes inactive on and after that date. -7. Human-readable responses include contextual next-step hints under \`help:\` when there is a useful follow-up - follow them. -8. \`--json\` mutation responses skip \`help:\` and return the deterministic result object instead. - -## Commands - -\`\`\` -${extractCommandsBlock()} -\`\`\` - -Run \`npx -y tasks-axi --help\` for global flags, or \`npx -y tasks-axi --help\` for per-command usage. +Get every command, flag, and workflow from the live CLI - it is the single source of truth: -## Tips +- \`npx -y tasks-axi\` - dashboard of the current backlog +- \`npx -y tasks-axi --help\` - global usage +- \`npx -y tasks-axi --help\` - per-command usage -- Output is TOON-encoded and token-efficient; the long task body is truncated by default - the whole point is that \`list\` stays cheap. - Use \`--full\` only when you need the complete notes. -- Every write leads with an \`ok:\` line confirming the write result, including the resulting task state when the command changes one (e.g. \`ok: start -> In flight\`, \`ok: done -> Done (pr )\`, \`ok: render -> normalized \`), then state-aware next-step hints. - Mutations are idempotent and add \`already: true\` on a no-op; re-running is safe. -- Pass \`--json\` to any mutation (\`add\`, \`start\`, \`done\`, \`reopen\`, \`update\`, \`rm\`, \`block\`, \`unblock\`, \`hold\`, \`unhold\`, \`mv\`, \`prune\`, \`render\`) for a machine-readable result object (\`{ "ok": true, "action": ..., "task": { ... } }\` or operation-specific result fields) instead of TOON - confirm a write deterministically without a follow-up read. -- \`block --by \` and \`unblock\` manage the dependency graph; \`hold --reason "" [--until YYYY-MM-DD]\` and \`unhold\` manage structured dispatch pauses; \`ready\` lists only queued work with no unresolved blocker and no active hold. -- Filter \`list\` with \`--state\`, \`--repo\`, \`--kind\`, \`--blocked\`, \`--limit\`, and add columns with \`--fields a,b,c\`. - Use \`list --state held\` or \`--fields held,hold_reason,hold_kind,hold_until\` when scanning active hold state. -- Existing prose markers such as \`HELD\`, \`PARKED\`, \`DEFERRED\`, \`CAPTAIN-DECISION\`, and \`do not dispatch\` stay prose until intentionally migrated. - Preserve the original prose as the hold reason, then choose \`captain\`, \`parked\`, \`future\`, \`load\`, or \`external\` only when the text supports that bucket. -- Note writes are inspect-then-update: run \`show --full\`, then replace the curated current body with \`update --body ""\` or \`--body-file \`. - Add \`--archive-body\` to preserve the superseded body in \`note-archive.md\`; \`--title ""\` replaces the title; \`render\` normalizes the file; \`mv [...] --to \` moves one or more tasks to another backlog in one atomic transaction - pass a whole connected set (a blocker and its dependents) to move it together and preserve its \`blocked-by\` links and reason strings; moves that would strand an endpoint are refused. -- Free-form (no-id) backlog lines are preserved verbatim and are never modified. +You do not need tasks-axi installed globally. If the CLI prints a follow-up starting with \`tasks-axi\`, run it as \`npx -y tasks-axi ...\` instead. `; } diff --git a/test/skill.test.ts b/test/skill.test.ts index d4e144b..a46d24b 100644 --- a/test/skill.test.ts +++ b/test/skill.test.ts @@ -1,31 +1,30 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { TOP_HELP } from "../src/cli.js"; -import { - createSkillMarkdown, - extractCommandsBlock, - SKILL_DESCRIPTION, -} from "../src/skill.js"; +import { DESCRIPTION } from "../src/cli.js"; +import { createSkillMarkdown, SKILL_DESCRIPTION } from "../src/skill.js"; function normalizeLineEndings(value: string): string { return value.replace(/\r\n/g, "\n"); } describe("skill generation", () => { - it("extracts the commands block from TOP_HELP", () => { - const block = extractCommandsBlock(); - expect(block).toContain("commands["); - expect(block).toContain("add, list, show"); - // the block is a slice of the canonical help, so it can never drift - expect(TOP_HELP).toContain(block); + it("keeps frontmatter identity and defers instructions to the CLI", () => { + const md = createSkillMarkdown(); + expect(md.startsWith("---\nname: tasks-axi\n")).toBe(true); + expect(md).toContain(JSON.stringify(SKILL_DESCRIPTION)); + expect(md).toContain("metadata:"); + expect(md).toContain(DESCRIPTION); + expect(md).toContain("`npx -y tasks-axi`"); + expect(md).toContain("`npx -y tasks-axi --help`"); + expect(md).toContain("`npx -y tasks-axi --help`"); }); - it("renders frontmatter and the shared guidance", () => { + it("does not bake CLI-owned command, flag, or workflow text", () => { const md = createSkillMarkdown(); - expect(md).toContain("name: tasks-axi"); - expect(md).toContain(JSON.stringify(SKILL_DESCRIPTION)); - expect(md).toContain("npx -y tasks-axi"); - expect(md).toContain("## Commands"); + expect(md).not.toContain("## Commands"); + expect(md).not.toContain("## Tips"); + expect(md).not.toContain("## Workflow"); + expect(md).not.toMatch(/^commands\[\d+\]:/m); }); it("matches the committed skill file (guards against drift)", () => {