From 5303b6caecf35507728c0b87bf1328a2333f8b5c Mon Sep 17 00:00:00 2001 From: T Date: Mon, 14 Sep 2026 18:30:26 +0200 Subject: [PATCH 1/7] [#217] feat: tag-driven workflows, opt-in automation on tagged issues Squashed rebase onto 698138b7 (post-#473). 11 commits -> 1. Risk: red (new automation surface: tag dispatch, card lock, audit). Intended changes: 42 files (#217 scope only). Refs: #217 --- ...omicity-primitives-use-node-fs-directly.md | 51 +++ ...-is-an-observation-not-a-malformed-flag.md | 48 ++ ...ven-dispatch-agnostic-core-host-adapter.md | 86 ++++ .pair/adoption/tech/architecture.md | 9 + .pair/adoption/tech/way-of-working.md | 81 +--- .../collaboration/automation/README.md | 4 +- .../automation/automation-policy.md | 103 ++++- .../automation/github-automation.md | 124 +++++- .pair/llms.txt | 181 +++----- apps/pair-cli/src/cli.e2e.test.ts | 278 +++++++++++- .../src/commands/run/automation-policy.ts | 137 ++---- .../src/commands/run/card-lock.test.ts | 166 +++++++ apps/pair-cli/src/commands/run/card-lock.ts | 207 +++++++++ .../src/commands/run/dispatch-audit.test.ts | 129 ++++++ .../src/commands/run/dispatch-audit.ts | 111 +++++ .../src/commands/run/dispatch.test.ts | 347 +++++++++++++++ apps/pair-cli/src/commands/run/dispatch.ts | 256 +++++++++++ .../pair-cli/src/commands/run/handler.test.ts | 415 ++++++++++++++++++ apps/pair-cli/src/commands/run/handler.ts | 310 ++++++++++++- .../src/commands/run/invocation.test.ts | 200 ++++++++- apps/pair-cli/src/commands/run/invocation.ts | 116 ++++- apps/pair-cli/src/commands/run/metadata.ts | 25 +- apps/pair-cli/src/commands/run/parser.test.ts | 86 ++++ apps/pair-cli/src/commands/run/parser.ts | 100 ++++- .../src/commands/run/perimeter.test.ts | 39 +- apps/pair-cli/src/commands/run/perimeter.ts | 39 +- .../src/commands/run/policy-sections.ts | 108 +++++ .../src/commands/run/resolve-skill.ts | 10 +- .../src/commands/run/routing-purity.test.ts | 90 ++++ .../src/commands/run/workflow-mapping.test.ts | 159 +++++++ .../src/commands/run/workflow-mapping.ts | 162 +++++++ .../content/docs/concepts/adoption-files.mdx | 75 +--- .../content/docs/reference/cli/commands.mdx | 251 +++++++---- .../docs/reference/guidelines-catalog.mdx | 6 +- .../docs/tutorials/unattended-delivery.mdx | 69 ++- .../collaboration/automation/README.md | 4 +- .../automation/automation-policy.md | 103 ++++- .../automation/github-automation.md | 124 +++++- .../automation-eligibility.test.ts | 394 ++++++++++++++++- .../src/conformance/github-automation.test.ts | 399 +++++++++++++++++ scripts/smoke-tests/lib/ci-tests.sh | 2 +- .../scenarios/github-dispatch-adapter.sh | 168 +++++++ 42 files changed, 5248 insertions(+), 524 deletions(-) create mode 100644 .pair/adoption/decision-log/2026-08-30-atomicity-primitives-use-node-fs-directly.md create mode 100644 .pair/adoption/decision-log/2026-08-30-empty-card-tags-is-an-observation-not-a-malformed-flag.md create mode 100644 .pair/adoption/tech/adr/adr-024-tag-driven-dispatch-agnostic-core-host-adapter.md create mode 100644 apps/pair-cli/src/commands/run/card-lock.test.ts create mode 100644 apps/pair-cli/src/commands/run/card-lock.ts create mode 100644 apps/pair-cli/src/commands/run/dispatch-audit.test.ts create mode 100644 apps/pair-cli/src/commands/run/dispatch-audit.ts create mode 100644 apps/pair-cli/src/commands/run/dispatch.test.ts create mode 100644 apps/pair-cli/src/commands/run/dispatch.ts create mode 100644 apps/pair-cli/src/commands/run/policy-sections.ts create mode 100644 apps/pair-cli/src/commands/run/routing-purity.test.ts create mode 100644 apps/pair-cli/src/commands/run/workflow-mapping.test.ts create mode 100644 apps/pair-cli/src/commands/run/workflow-mapping.ts create mode 100644 packages/knowledge-hub/src/conformance/github-automation.test.ts create mode 100755 scripts/smoke-tests/scenarios/github-dispatch-adapter.sh diff --git a/.pair/adoption/decision-log/2026-08-30-atomicity-primitives-use-node-fs-directly.md b/.pair/adoption/decision-log/2026-08-30-atomicity-primitives-use-node-fs-directly.md new file mode 100644 index 000000000..bbbeddeae --- /dev/null +++ b/.pair/adoption/decision-log/2026-08-30-atomicity-primitives-use-node-fs-directly.md @@ -0,0 +1,51 @@ +# Decision: the two atomicity primitives (exclusive create, append) use `node:fs` directly, in leaf modules tested against a real temporary directory + +## Date + +2026-08-30 + +## Status + +Active + +## Category + +Convention Adoption + +## Context + +Story #217's dispatch needs two filesystem operations whose whole value is **atomicity**: + +- an **exclusive create** — the per-card lock that guarantees a trigger burst never starts two runs on one card; +- an **append** — the audit trail, whose lines must survive two dispatches writing the same file concurrently. + +The project's convention is dependency injection through `FileSystemService`, with an `InMemoryFileSystemService` double instead of mocks. That service exposes neither primitive: `mkdirSync` is modelled in the double as "add the path to a set" (it cannot fail a second create at all), and the only write is `writeFile`, a full overwrite — so an append would have to be read-concat-write, which reintroduces exactly the lost update `O_APPEND` exists to prevent. + +Widening `FileSystemService` was the obvious alternative, and it is the one worth stating why we did not take. + +## Decision + +`card-lock.ts` and `dispatch-audit.ts` call `node:fs` **directly** (`mkdirSync` without `recursive`, `appendFileSync`), and are: + +- **leaf modules** — nothing else in the dispatch path touches the filesystem, so the untestable surface is two small files rather than a layer; +- **injected at the call site** — the handler takes a `LockAcquirer` and an `AuditAppender`, so every other test in the run pipeline stays hermetic and none of them touches a real working area; +- **tested against a real temporary directory** (`mkdtempSync`), because the properties under test — a second create fails, two appends both survive — are properties of the real filesystem and of nothing else. There is precedent in this repo: `path-containment.test.ts` tests symlink containment the same way, for the same reason. + +The rule generalises: **when the behaviour under test IS an atomicity or containment guarantee of the operating system, test it against the operating system.** A double that cannot fail the way production fails proves nothing, and asserting against it is worse than not asserting — it reads like coverage. + +## Alternatives Considered + +- **Add `mkdirExclusive`/`appendFile` to `FileSystemService`**: correct in principle, but it widens a package shared by every other story in flight for two callers, and the in-memory double would still have to *simulate* the failure mode — so the double's fidelity, not the filesystem's behaviour, is what the tests would end up asserting. Reconsider when a third caller appears. +- **Read-concat-write the audit through `writeFile`**: loses records when two dispatches on different cards write the same audit file; the per-card lock does not protect a shared file. +- **Lock with `existsSync` + `mkdirSync`**: a check-then-act window, which is the exact race the lock exists to close. + +## Consequences + +- Two modules in `apps/pair-cli/src/commands/run/` bypass `FileSystemService`, each carrying a comment saying why and pointing here. +- Their tests are slower than the rest of the suite (real I/O in `os.tmpdir()`), and clean up after themselves. +- Handler-level tests inject fakes for both, so the dispatch pipeline remains testable in memory. +- A future third caller for either primitive is the trigger to revisit and put it on `FileSystemService` properly. + +## Adoption Impact + +- `adoption/tech/way-of-working.md` — Quality Gates section: records the exception to the "avoid mocks, use the in-memory double" convention for OS atomicity/containment guarantees. diff --git a/.pair/adoption/decision-log/2026-08-30-empty-card-tags-is-an-observation-not-a-malformed-flag.md b/.pair/adoption/decision-log/2026-08-30-empty-card-tags-is-an-observation-not-a-malformed-flag.md new file mode 100644 index 000000000..8b02795b7 --- /dev/null +++ b/.pair/adoption/decision-log/2026-08-30-empty-card-tags-is-an-observation-not-a-malformed-flag.md @@ -0,0 +1,48 @@ +# Decision: an empty `--card-tags` means "this card carries no labels", not a malformed flag + +## Date + +2026-08-30 + +## Status + +Active + +## Category + +Convention Adoption + +## Context + +`pair run` refuses every flag passed with an empty value — `--root ""`, `--filter ""`, `--skill ""` all fail at parse time, deliberately: a flag named with nothing behind it is a caller bug, and accepting it silently is how an unattended run ends up doing something nobody asked for. + +Story #217's `--card-tags` inherited that rule, and the end-to-end test on a populated board (T5) showed it was the wrong rule for this one flag. The dispatch entry point is called by a **host trigger**, and the reference GitHub adapter renders the labels it observed as `join(github.event.issue.labels.*.name, ',')`. On an issue with **no labels** that expression renders `""`. So the very state AC2 is about — "an issue with no mapped tag runs nothing" — arrived at the parser as an empty value and was rejected with `--card-tags was passed with an empty value`, exit 1. + +Two consequences, both bad, and neither visible from inside the module suites (they pass tag lists, not the empty string a host renders): + +- the opt-in boundary of the whole feature — untagged ⇒ skipped, reported, exit 0 — became **unreachable through the entry point**; +- the commonest card on any board turned every trigger firing on it into a **failed CI job**, which is the noise that gets a trigger disabled. + +## Decision + +For `--card-tags`, and only for it, an **empty or whitespace-only value is data**: it is read as the observation "the trigger saw no labels on this card", producing an empty tag list. The dispatcher then does what it does for any card with no mapped tag — skips it, reports the reason, appends the skip to the audit trail, exits `0`. + +A **hole inside a list** stays an error: `auto-dev,,risk:green` still HALTs. The two cases are genuinely different. An empty value is a complete observation of an empty set; a hole is an incomplete rendering of a non-empty one — the caller built a list and lost an item, which is exactly the string-interpolation bug worth failing on. + +The general rule this instantiates: **a flag that carries an observation from an external system is empty-valid when the empty case is a real state of that system; a flag that carries an operator's intent is not.** `--root`, `--filter` and `--skill` are intent — nobody means "" by them. `--card-tags` is an observation, and "no labels" is a state of every board. + +## Alternatives Considered + +- **Keep the refusal, make the adapter skip the call when the label list is empty**: pushes an authorization-relevant decision — "should this card run?" — into every per-host adapter, where it is untested, duplicated per host, and free to drift. ADR-024 puts that decision in the routing core precisely so no adapter can widen or narrow it. +- **Keep the refusal, have the adapter pass a sentinel** (`--card-tags "(none)"`): invents a label that could collide with a real one and makes the trail lie about what the trigger saw. +- **Accept empty values on every flag**: loses the guard where it earns its keep — an empty `--root` or `--skill` is a caller bug with no legitimate reading. + +## Consequences + +- `apps/pair-cli/src/commands/run/parser.ts` reads an empty/whitespace `--card-tags` as an empty tag list; the empty-entry HALT for a hole inside a list is unchanged. +- An unlabelled card now produces the documented skip and exit `0` end-to-end, so a host adapter needs no pre-filter and no conditional call. +- The asymmetry between this flag and its neighbours is deliberate and must stay documented where a reader meets it: the parser module, the CLI reference, and the reference adapter in the KB. + +## Adoption Impact + +- `adoption/tech/way-of-working.md` — CLI conventions: records that flags carrying an external observation are empty-valid when the empty case is a real state of the observed system, while flags carrying operator intent are not. diff --git a/.pair/adoption/tech/adr/adr-024-tag-driven-dispatch-agnostic-core-host-adapter.md b/.pair/adoption/tech/adr/adr-024-tag-driven-dispatch-agnostic-core-host-adapter.md new file mode 100644 index 000000000..3d8ec5304 --- /dev/null +++ b/.pair/adoption/tech/adr/adr-024-tag-driven-dispatch-agnostic-core-host-adapter.md @@ -0,0 +1,86 @@ +# ADR-024: Tag-driven dispatch — the mapping is adoption, the routing core is host-agnostic, the on-issue record belongs to the host adapter + +## Status + +Accepted + +## Date + +2026-08-30 + +## Context + +- Story #217 (R4.4, epic #212) asks for **tag-driven workflows**: different tags trigger different workflows, exclusively on tagged issues, with the tag→workflow mapping declared in adoption. Automation must start **only** where a team explicitly enabled it, and each tag must route to the right behavior. +- The pieces around it already exist and must not be re-decided: `## Eligibility` (#216) selects *which cards* an unattended run may pick up; `pair-next` stays the frozen selector atom (ADR-017 §1); `pair run` is the portable execution adapter and the ADR-021 tier-2 entry point (#451); `pair-loop` and the other process skills are the workflows themselves. +- What was **not** decided anywhere: where the tag→workflow mapping lives, who evaluates it, how the "run start is recorded on the issue" (AC3) happens given that the driver deliberately holds **no tracker credentials**, and how a trigger burst is prevented from starting two runs on one card. +- **Hard to reverse**: the mapping's grammar becomes adoption content in every adopting project, and the entry point's flags become a public CLI contract that host triggers are written against. +- **Surprising without context**: that the driver is *told* the card's labels instead of reading them from the tracker; that an unmapped card is skipped by *absence of a route* rather than by a guard; and that the on-issue comment is emitted as a line on stdout for someone else to post. +- **Real trade-off**: the alternatives (a dispatcher inside the loop skill, a tracker client inside the CLI) were both available and were rejected for reasons recorded below. + +## Options Considered + +### Option 1: routing inside the loop skill (`pair-loop` reads the mapping and decides) + +- **Description**: the mapping stays in `tech/automation.md`, but the agent skill reads it, matches tags and picks the workflow. +- **Pros**: no new CLI surface; the skill already reads the policy file. +- **Cons**: the routing decision — an **authorization** decision, since "untagged ⇒ never" is the opt-in boundary — would live in prose executed by an LLM, with no test able to pin it. Every safety property of the story (untagged never runs, no default workflow, no silent multi-tag choice) would be unverifiable by anything but another prompt. + +### Option 2: the CLI grows a tracker client and reads the card's labels itself + +- **Description**: `pair run --card 217` fetches the issue from the code host, reads its labels, then routes. +- **Pros**: one argument instead of two; the operator cannot pass stale labels. +- **Cons**: puts host credentials and a per-host API client into the driver, which is the one component that is deliberately host-agnostic — and multiplies by every tracker pair supports. It also duplicates what the trigger already knows: a host workflow firing on a label event *has* the labels in hand. + +### Option 3 (chosen): adoption mapping + a pure routing core in the entry point, fed by a thin per-host adapter + +See Decision. + +## Decision + +1. **The mapping is adoption data**: a seventh section of the optional `.pair/adoption/tech/automation.md`, `## Workflows`, with entries `` and an optional `Precedence:` line. Its schema is owned by the KB guideline `collaboration/automation/automation-policy.md` (D21: adoption is the delta, the KB is the schema). The **tag is an opaque routing key** and the **workflow is a skill name** — so no classification criterion ever lives in code (D18). The *set of nameable workflows* is not open, and item 7 is why: it is the KB catalog, held in the driver as data and asserted equal to the guideline's table by test. + +2. **The routing core is a pure function in the entry point** (`pair run`, ADR-021 tier 2): `dispatch.ts` takes the card, the labels a trigger observed, the policy and an installed-skill probe, and returns *route* or *skip*, or HALTs. It performs no I/O, holds no credentials, and knows nothing about the tracker. The order is normative — **mapping → eligibility → routing** — so an ineligible card is skipped before its tags are read at all. + +3. **The card's labels are an input, not a lookup**: `pair run --card --card-tags `. The trigger's own **thin per-host adapter** (a GitHub Actions job, a webhook runner) supplies both, under the credentials it already runs with. Adding a code host is a new adapter, never a change to the core. Both values are untrusted host data and are content-checked at parse time, exactly as `--root`/`--filter` already are. + +4. **The on-issue audit is split, and the split is the point**: every decision (start/skip/end) is appended to the run's `## Audit Location` file, and the `start` record — **and only that one** — is *also* printed as a single `DISPATCH-RECORD:` line for the host adapter to post as a comment on the card. The driver writes files and prints lines; it never posts to a tracker. Skips and ends stay in the file deliberately: a card that gets a comment for every unmapped label edit is unreadable within a day, and the `end` duplicates on the card what the trail already holds. AC3 asks for the run *start* on the issue, and that is exactly what ships. + +5. **A trigger burst never starts two runs on one card**: the dispatch takes an **exclusive per-card lock** (an atomic `mkdir` under `working_path`) before spawning and releases it unconditionally afterwards — including when the run throws, which also writes the `end` record (`outcome=crashed`) rather than leaving the trail stopped at `start`. A locked card is **skipped and logged**, never queued — it is still tagged, so the next trigger picks it up — and the skip reports the holder's directory and how long it has been held, because nothing reaps a lock (see the limitation below). + +6. **Fail-safe everywhere, in one direction**: no `## Workflows` section ⇒ "no mapping declared", clean exit; no mapped tag ⇒ skip; ineligible ⇒ skip; a workflow that is not installed, a workflow whose scoping argument the driver cannot spell (item 7), or a multi-tag card with no covering `Precedence:` ⇒ **HALT** with an adoption-fix message. Nothing ever falls back to a default workflow. + +7. **A dispatched card IS the run's scope — under the routed workflow's own name for it, and nothing displaces it.** The card travels as an argument the workflow declares (`--root` for `pair-loop`, `--story` for `pair-process-plan-tasks`), borrowed from its `## Arguments` table and never invented (D18); the mapping from the driver's scope slot to each workflow's spelling is DATA in `invocation.ts`, pinned against the dataset corpus by a test. Three refusals hold that property up, and all three are needed: + - **`--root` (and `--skill`/`--prompt`) alongside `--card` is refused at parse time.** A dispatched card is the whole answer to "what is this run about", and an operator or wrapper flag answering it a second time is not a narrowing: `--card 217 --root 300` would drive the agent over subtree 300 while the audit trail, the `DISPATCH-RECORD:` comment and the exclusive lock all named 217 — 300 unguarded, 217 credited with work nothing did on it. The handler additionally reads the dispatched card *before* `config.scope.root`, so the outcome stays unreachable for a caller that skips the parser. + - **A mapped workflow outside the KB catalog is refused**, even when installed and even when the driver knows how it spells its scope. The mappable set (`DISPATCHABLE_WORKFLOWS`) is its own declaration, deliberately NOT derived from the argument table: `pair-next` has a row there because `--skill pair-next --root 212` is a legitimate hand-driven run, and routing a card to it would take the card's lock and post a `DISPATCH-RECORD:` comment for a run that prints a recommendation and changes nothing. The set is asserted EQUAL to the guideline's catalog table, in both directions. + - **A catalogued workflow the driver holds no scoping row for is refused.** An argument a skill does not declare is *ignored*, not rejected, and the `pair-process-*` workflows then select the highest-priority story on the board themselves — the run works a card nobody tagged while the trail names the card that was. + + Refusing is in every case the only outcome that keeps item 4's record true. + +8. **The mappable set admits only workflows that can finish with nobody watching.** A dispatch spawns its workflow under the operator's one-time `--autonomous` opt-in, holds the card's exclusive lock for the run, and has already posted a public `DISPATCH-RECORD:` comment saying a run started. A workflow whose own SKILL.md requires an explicit human decision has exactly two outcomes there, and both are worse than not running: it **stalls** on a question no one answers until the per-iteration timeout, or the agent — having no interlocutor — **supplies its own approval** and drives the card past the gate, satisfying the authorization control with the party it exists to constrain. So `pair-process-refine-story` is NOT mappable, even though the driver knows exactly how it spells its scope (`--story`): it is the single Draft→Ready path, its phase 0 is "the R3.11 AI↔human alignment gate — a prerequisite, not optional", it adds three per-step `Human-judgment gate`s, and it states that "what is never skipped is explicit human alignment before the story reaches `Ready`" (R3.11, D24). It keeps its `SKILL_PARAMETERS` row, because `--skill pair-process-refine-story --root ` is a legitimate HAND-DRIVEN run — someone is there to answer. The rule is enforced against the skills' own SKILL.md by a KB conformance guard, so putting a row back into the catalog table fails a test rather than shipping. + + Deliberately NOT enforced via `$approval`: none of the mappable workflows declares that argument either (`pair-loop` composes the family that does, `pair-process-plan-tasks` has no approval round at all), so a "must declare `$approval`" gate would refuse the whole catalog. What distinguishes the excluded case is a human-judgment gate in its own steps, not an argument on its interface. + +## Consequences + +### Benefits + +- Every safety property of the feature is a **tested production module**, not prose: untagged-never, eligibility-before-routing, no-silent-choice and one-run-per-card each have unit tests, and the KB's normative claims have a conformance guard. +- The driver stays credential-free and tracker-agnostic; pair can gain a host by gaining an adapter. +- The mapping composes existing skills, so a "workflow" costs a line of adoption rather than an engine. +- `pair-next` and the eligibility filter are untouched — dispatch narrows what runs, it never widens what is selected. + +### Trade-offs and Limitations + +- **Labels are as fresh as the trigger that passed them.** A card whose tag changed between the trigger firing and the dispatch starting is routed on the observed value. Accepted: re-reading them would require the tracker client this ADR exists to avoid, and the eligibility label is re-checked by the invoked skill on every iteration anyway. +- **`--card-tags` is comma-separated**, so a label containing a comma is not routable. Same over-inclusive direction `## Eligibility` already takes: the fix is to rename or re-project the label, never to widen the separator. +- **The on-issue comment is the adapter's job**, so a project whose adapter does not post it gets the file trail only. Documented per host in the KB rather than silently degraded. +- **Tag-driven automation does not cover refinement** (item 8). A team wanting Draft→Ready unattended gets nothing from this feature: the only Draft→Ready path requires a human, so refinement stays hand-driven (or batch-driven with a human present). Accepted rather than worked around — an unattended path past that gate would be a change to D24, not to this ADR. +- **The lock is filesystem-local**: two runners on different machines sharing no working area can still collide. Bounded by the same working area every other run artifact already assumes; a distributed lock is out of scope and out of the story's stated isolation model. **Consequence for the reference adapter**, stated because it inverts what a reader assumes: on GitHub-hosted runners every job checks out a fresh workspace, so the lock can never observe a holder from another job and the host's `concurrency` group is the cross-job guard there. Every path that dispatches a card must sit in that group; the per-card lock is the guard on the *persistent-daemon* deployments (the tutorial's Options A–C), where the host offers none. +- **Nothing reaps a lock.** A run killed by SIGKILL, an OOM kill or a job timeout leaves the directory behind, and every later trigger on that card then skips, exits `0` and looks exactly like a healthy burst — automation silently off for one card. Mitigated, not solved: the skip prints the holder's path and age (`holder.json`'s `acquiredAt`), and the KB's pre-flight documents clearing it. A TTL was rejected as the wrong default — a lock that expires while its run is alive re-creates the race the lock exists for, and no timeout is right for every workflow a mapping can name. +- **A mapping naming an uninstalled workflow — or one outside the KB catalog — HALTs the whole board**, not just the cards carrying that tag: both checks run before eligibility and routing. The second bounds what a mapping may name to the catalog's two: a project mapping a tag to any other skill is refused rather than dispatched blind, and widening that set is a deliberate change to the guideline's table plus the one-line data edit the equality assertion then demands. Deliberate — a broken mapping is broken configuration, and surfacing it only on whichever card happens to carry the tag would make the failure depend on which trigger fired first — but the blast radius is a property adopters must be told about, so it is stated in both the schema and the adapter's pre-flight. + +## Adoption Impact + +- `adoption/tech/architecture.md` — records tag-driven dispatch as the entry point's routing layer, and the agnostic-core / host-adapter boundary. +- `adoption/tech/automation.md` — **unchanged on purpose**: this project declares no `## Workflows` section, so tag-driven dispatch stays off here. The absent-section path is the shipped default and the one this repo exercises. +- KB (`packages/knowledge-hub/dataset/.pair/knowledge/...` + the root mirror) — `automation-policy.md` gains the `## Workflows` schema; `github-automation.md` gains the reference host adapter. diff --git a/.pair/adoption/tech/architecture.md b/.pair/adoption/tech/architecture.md index 12d006c28..4ce2a623f 100644 --- a/.pair/adoption/tech/architecture.md +++ b/.pair/adoption/tech/architecture.md @@ -38,6 +38,15 @@ - Canonical target (`.claude/skills/`) receives physical copies; secondary targets receive symlinks. - Windows environments fall back to copy mode (symlinks rejected at validation time). See [ADR-005](adr/adr-005-skills-infrastructure.md). +## Unattended Dispatch + +- **Tag-driven dispatch is opt-in, per card, and declared in adoption.** `## Workflows` in `tech/automation.md` maps a tag to the workflow (a skill name) that runs on a card carrying it; the tag is an opaque routing key and no classification criterion ever lives in the routing code (D18). A card with no mapped tag runs nothing — there is no default workflow. See [ADR-024](adr/adr-024-tag-driven-dispatch-agnostic-core-host-adapter.md). +- **The routing core is host-agnostic and credential-free.** It lives in the `pair run` entry point (ADR-021 tier 2) as a pure function over the card, the labels a trigger observed, and the policy; the labels are an INPUT (`--card`/`--card-tags`) supplied by a thin per-host trigger adapter, never fetched by the driver. Adding a code host is a new adapter, never a change to the core. +- **A dispatched card reaches its workflow under that workflow's own argument name** (`--root` for `pair-loop`, `--story` for `pair-process-refine-story`/`pair-process-plan-tasks`), borrowed from its `## Arguments` table and never invented (D18). A mapping naming a workflow outside the KB catalog — or one the driver holds no such row for — **HALTs** with the uninstalled-workflow check, before eligibility and routing: an undeclared argument is ignored rather than rejected, and a workflow that picks its own subject when unscoped would then work a card nobody tagged while the trail below named the card that was. The mappable set is a declaration of its own (`DISPATCHABLE_WORKFLOWS`), asserted equal to the guideline's catalog table — knowing how a skill spells its scope is not what makes a tag allowed to route a card to it, and neither does it license routing to a workflow that needs a human: `pair-process-refine-story` is scopable, hand-drivable and deliberately NOT mappable, because its alignment gate ends only on an explicit human approval (ADR-024 item 8). +- **Nothing displaces the dispatched card as the run's scope.** `--root`, like `--skill`/`--prompt`, is refused alongside `--card` at parse time, and the handler reads the dispatched card before `config.scope.root`: an operator flag that silently outranked the mapping would drive the agent over one subtree while the audit trail, the on-issue record and the exclusive lock all named another (ADR-024 item 7). +- **The audit trail is split accordingly**: every decision is appended to the `## Audit Location` file, and the run-start record — only that one — is printed as a `DISPATCH-RECORD:` line for the host adapter to post on the card. Skips and endings stay in the file. +- **Never two runs on one card, within one working area**: a dispatch takes an exclusive per-card lock under `working_path` before spawning and releases it unconditionally (a crash writes `outcome=crashed` on the way out); a trigger burst is skipped and logged, never queued. The lock is filesystem-local, so a host that gives every job a fresh checkout needs its own concurrency group as the cross-job guard — see ADR-024's limitations. + --- All architectural implementations must follow these adopted standards. For process and rationale, see [way-of-working.md](../../way-of-working.md). diff --git a/.pair/adoption/tech/way-of-working.md b/.pair/adoption/tech/way-of-working.md index 437d29ae4..d18b95c89 100644 --- a/.pair/adoption/tech/way-of-working.md +++ b/.pair/adoption/tech/way-of-working.md @@ -17,12 +17,6 @@ **Nothing declared here — both keys are at their defaults**, and `.pair/adoption/` is delta-only (D21, ADR-018): a key belongs in this section only when it differs from the default. `code-host` omitted ⇒ the code host **is** the PM tool (GitHub Projects hosts the repo), so this is the zero-configuration single-tool path — no dual-write, no cross-link comment, every PR/review operation on GitHub; `base-branch` omitted ⇒ `main`. A split setup (e.g. Linear for the backlog + GitHub for the code) is what makes `code-host` load-bearing. Schema and resolution rule: [way-of-working / PM-tool + code-host resolution](../../knowledge/guidelines/technical-standards/ai-development/skill-conventions/way-of-working-pm-resolution.md). -## Process Profile - -**Nothing declared here — the profile is `default`**, and `.pair/adoption/` is delta-only (D21, ADR-018): pair runs the full process end to end (it is the project that produces it), so there is no subset to declare and every catalogued step stays enabled. The section exists as the anchor a reader looks for, exactly like `## Git Workflow` above. - -A project that runs a subset declares it here — `profile: poc`, or `profile: custom` with a `whitelist` of step ids. Schema, built-ins and error cases: [process-profiles.md](../../knowledge/guidelines/technical-standards/ai-development/process-profiles.md); the step ids and their two representations: [step-catalogue.md](../../knowledge/guidelines/technical-standards/ai-development/step-catalogue.md). - ## Story Closure (post-merge) Per merged PR, in order: **1)** squash merge, message per [commit template](../../knowledge/guidelines/collaboration/templates/commit-template.md) · **2)** story: check the DoD boxes in the body, close (`completed`), write board state `Done` ([State Mapping](#state-mapping)) · **3)** cascade: epic/initiative close only when ALL sub-issues are Done · **4)** cleanup: delete branch **remote AND local** (remove any worktree holding it first), remove the story checkpoint and the PR analysis under `working/pr-analyses/` · **5)** optional P0 manual tests. @@ -62,64 +56,6 @@ Resolution order, the split-tool routing and why the fallback is never the authe ## Review Convergence -- **Negative transition matrix before implementation (ADR-024, amendment 2026-09-11 l; US-479 - S12/AC-30):** a task that creates or changes a persisted state, a ledger transition, a terminal - gate or an evidence identity contracts the whole finite matrix upstream — the positive path plus - every applicable negative family, each with a deterministic witness and a typed expected refusal. - The independent verifier receives the same authority the resolver holds and rejects an incomplete - matrix before the seal; implementation cannot begin without it; the final review samples the sealed - rows rather than being the first control to discover an illegal transition. A composed green chain - is necessary and not sufficient. -- **Rollback is a maintainer's call, and its notes are derived (ADR-024 amendments r/s; ADL - 2026-09-12; US-479 S13/AC-32):** the rewind fixes forward on the current head by default. A - maintainer may instead name the HEAD to roll back to — 40-hex, read from `git log` — and the - producing group's own `allowedPaths` are restored to that content and rebuilt, still committing - FORWARD. The workflow neither picks the point nor vetoes the choice: it once decided whether a - restore was safe, and four independent reviews found four defects in that decision. A head this - cycle never recorded is refused out loud and the run STOPS on it: the refusal travels as a field on - the dispatch and the coordinator ends the story `failed-preparation`, rather than the directive - being computed and dropped. The directive STANDS while the policy names the head, and the - MAINTAINER clears it (amendment (u)): the workflow does not infer whether their decision was - carried out — nothing in the handoffs records that, and four attempts to deduce it produced four - blocking defects, each failing one staging beyond the last. What it owes instead is legibility, - and every delivery is reported in the run log with the head, the paths restored and whose job it - is to end it. A directive still standing later is a visible state, not a predicate misfiring. - The notes a rebuild needs — obligations still open, regressions still live, and the decisions the - review verified were RIGHT (`worked`) — are a VIEW over the handoffs: nobody writes them to a - second place and nobody deletes them. Amendment (u) and the 2026-09-12 ADLs are current here; (t) - stands except for the spend rule (u) withdrew. -- **Reintroduction (ADR-024, amendment 2026-09-11 o; US-479 S13/AC-31):** reopening a discharged - risk is a RESTORATION of its prior ledger entry — every field immutable. A defect that reappears - through later work is a new risk with a new identity, not a reopening. -- **Regression-risk rewind (ADR-024, amendment 2026-09-11 k; US-479 S11/AC-29):** a defect a review - proves was INTRODUCED by a remediation invalidates that remediation and sends the cycle back to the - same batch's preparation, carrying every unresolved finding and every active guard in one complete - corrective contract. This "rewind" is a workflow-state transition only — the branch stays on its - current head, the fix goes FORWARD, and `git revert`/`reset`/`rebase`/force-push and seal deletion - are never part of it (a maintainer may authorize a Git revert as a separate decision). The claim - needs the approved obligation, an executable reproducer passing on the last clean reviewed head and - failing on the first failing head, the introducing batch and the affected boundaries; anything less - is an ordinary finding of unknown origin, and a new requirement stays a scope proposal. Only an - independent review bound to the exact new head discharges a risk. Convergence, scope escalation and - ready-for-merge are impossible while the derived active matrix is non-empty; a discharged risk - leaves that matrix and stays in history and in the counters. - -- **Delivery-workflow canary:** prove a fresh workflow with a small code story - whose tests are deterministic oracles; a prose-only regex-guard story is not a - substitute. Keep the RED repair budget unchanged: a typed D2 refusal is valid - evidence, not a reason to weaken it. Record the run/phase handoffs, first - review and final synthesis on the reviewed PR. See ADL - [2026-09-09-deterministic-code-canary-for-delivery-workflow.md](../decision-log/2026-09-09-deterministic-code-canary-for-delivery-workflow.md). -- **Delivery workflow — four judgment stages, incremental resume (ADR-024, amendment 2026-09-09 b):** - the batch engine judges in four stages — preparation (inventory + executable acceptance contract, - before any production edit), independent contract validation with the deterministic seal in the - same execution, implementation, independent final verification (custody, evidence, review, tier - passes, one idempotent publication). Mechanical probe/seal/hash/state/comment work runs as scripts - inside those stages, never as its own dispatch. A same-input resume continues from the first - incomplete step and never re-samples a full review; an approved test failing on production returns - to implementation on the same seal; a real contract gap revises only the affected obligations. - External (card / PR-body) findings stay blocking until corrected with read-back evidence or - dispositioned by a human. See [adr-024](adr/adr-024-delivery-phases-are-skills.md). - **Baseline then delta:** the first review is complete and returns the immutable 40-character head it inspected. A re-review verifies prior findings plus only the diff from that head and directly changed producer/consumer boundaries; an unchanged PR surface does not create another @@ -128,30 +64,21 @@ Resolution order, the split-tool routing and why the fallback is never the authe artifact maps `producer -> published identity -> consumer` and proves the real path in a clean temporary environment. The exact boundary is never stubbed, aliased, or faked. See ADL [2026-08-31-review-baseline-and-provisioned-artifact-contract.md](../decision-log/2026-08-31-review-baseline-and-provisioned-artifact-contract.md). -- **Contract inventory before a loop:** before reporting or fixing a changed contract, inventory - its authoritative producer, inputs, consumers and representations. A finite protocol, parser, - configuration or state transition gets a complete decision table of supported and - invalid/boundary states, with a real probe/test per row. When a row, equivalence, normalization - or repair depends on an external tool/service/format, prove it at that authoritative boundary; - an internal unit test cannot prove external semantics or that repair advice works. Re-review - applies the same rule only to its delta and changed boundary. See ADLs - [2026-09-01-review-contract-inventory-prevents-serial-findings.md](../decision-log/2026-09-01-review-contract-inventory-prevents-serial-findings.md) - and [2026-09-01-external-boundary-proof-prevents-false-equivalence.md](../decision-log/2026-09-01-external-boundary-proof-prevents-false-equivalence.md). ## Quality Gates - `pnpm quality-gate` is the adopted project-level quality gate command. -- Quality gate includes: type checking (`ts:check`), testing (`test`), linting (`lint`), formatting and markdown lint in **check mode** (`format:check`), plus two composition guards under `gate:composition` (the gate stays check-mode — `pre-push-gate:check`; `format.yml` keeps its shape — `format-workflow:check`) and the smoke-scenario mode guard (`smoke-modes:check`). +- Quality gate includes: type checking (`ts:check`), testing (`test`), linting (`lint`), formatting and markdown lint in **check mode** (`format:check`), plus a guard that the gate stays check-mode (`gate:composition`) and the smoke-scenario mode guard (`smoke-modes:check`). - **`format:check`/`format` coverage is whole-repo, derived from git, not turbo's per-workspace scope** (#414): `scripts/format-lib/run-format.sh` lists every path `git ls-files --cached --others --exclude-standard` reports (extension-filtered), so "gitignored ⇒ never checked" is git's own rule — nested `.gitignore` files and the user's global `core.excludesFile` apply by construction, with no re-implementation in the wrappers. Coverage excludes almost nothing: root-level and non-workspace files (`.claude/**`, `.pair/adoption/**`, `qa/**`, `scripts/**`) are checked exactly like workspace files. **One documented exception**: third-party skills installed under `.claude/skills/` (any directory not matching the `pair-*` prefix, e.g. a marketplace skill) are never checked — their formatting is not this project's to maintain. An empty derived file set is treated as a broken wrapper (exit 2), never a silent pass — see `scripts/format-lib/git-tracked-paths.sh`. The per-package, glob-based invocation (`pnpm --filter prettier:check`/`mdlint:check`) is unaffected and still uses the wrappers' own `_ignore-args.sh`/`_ignore-file.sh` ignore assembly. -- **A guard whose only caller is a turbo task is not enforced.** `turbo ts:check test lint` are cacheable with package-scoped inputs, so a change OUTSIDE the guard's package replays a cached PASS and the guard never executes. A guard over repo-wide state therefore gets a thin CLI and a **root gate step** (`hygiene:check`, `smoke-modes:check`, `docs:staleness`, `skills:conformance`), which run unconditionally — a unit test alone is the enforcement point only for logic whose inputs live inside its own package (#400). **Second, lighter mechanism for the same guarantee**: declare the guarded artifact as a `$TURBO_ROOT$` **task input** in `turbo.json` (turbo >= 2.1), so the cache can no longer serve a stale PASS — used by `@pair/knowledge-hub#test` (the KB/skills/docs artifacts), and by `@pair/dev-tools#test` for `scripts/format-lib/**`, `.github/workflows/format.yml` and **the root `package.json`** — that third entry is not optional bookkeeping: both guards in that folder resolve script delegation against the root scripts (`checkThisRepoGate` parses it, `checkFormatWorkflow` defaults `rootScripts` to it), so without it rewriting root `format:check` to `pnpm prettier:fix` left the task hash unchanged at `7271baf2a672a276` — a cached PASS with neither guard running. Sufficient because CI is **cold on every run** (no remote cache, no `.turbo` restore in `ci.yml`), so `pnpm test` already executes the guard on every PR and the input entry closes the LOCAL false green — which is also what the pre-push hook sees. Pick per guard by what invalidates it: the CLI + root-step form stays **required** when the input set is not expressible as task inputs, or when a failure must report as its own CI status context rather than inside `pnpm test` — and a guard may use BOTH, as `format-workflow-composition` does (the input entry keeps `pnpm test` honest; `format-workflow:check` under the existing `gate:composition` segment is the mechanism #413's AC6 names). See ADL [2026-09-01-repo-wide-guard-enforced-by-turbo-root-input.md](../decision-log/2026-09-01-repo-wide-guard-enforced-by-turbo-root-input.md). +- **A guard whose only caller is a turbo task is not enforced.** `turbo ts:check test lint` are cacheable with package-scoped inputs, so a change OUTSIDE the guard's package replays a cached PASS and the guard never executes. A guard over repo-wide state therefore gets a thin CLI and a **root gate step** (`hygiene:check`, `smoke-modes:check`, `docs:staleness`, `skills:conformance`), which run unconditionally — a unit test alone is the enforcement point only for logic whose inputs live inside its own package (#400). - **No step reachable from the gate writes files**: the gate reports, `pnpm format` / `pnpm lint:fix` fix deliberately. `gate:composition` enforces this through an **explicit offender list** — the two formatters, eslint autofix, and the repo's write scripts (`sync-version`, `test:perf`) — so **adding a new write-mode script to this repo means adding it to that list**; a differently named writer passes the guard green. See ADL [2026-07-31-pre-push-gate-is-check-only.md](../decision-log/2026-07-31-pre-push-gate-is-check-only.md). - **Pre-merge tiering**: `disabled` (default) — every PR runs the full pre-merge check suite. Set to `enabled` to opt into risk-tier-scoped pre-merge checks (lighter checks on lower-risk PRs) per [tier-aware-pipeline.md](../../knowledge/guidelines/infrastructure/cicd-strategy/tier-aware-pipeline.md); `/pair-capability-setup-gates` reads this flag before generating the pipeline. - **Review enforcement**: `disabled` (default) — the pair review **runs and publishes its verdict**, but nothing it says blocks a merge: `pair-review` and `pair-explicit-approval` are not required status checks, and the 🔴 explicit-approval rule is advisory. Set to `enabled` to make them required and the rule binding, per [pr-states.md](../../knowledge/guidelines/collaboration/project-management-tool/pr-states.md); `/pair-capability-setup-gates` reads this flag before touching branch protection, and `/pair-process-bootstrap` asks for it when no decision exists. Disabled is the default deliberately: a review that blocks by default turns a first install into a repository nobody can merge into — on a single-maintainer repo the 🔴 non-author approval is unobtainable outright. The tier requirements themselves (reviewer count, SLA, checklist depth, whether 🔴 needs explicit approval) are redefinable in this file; that the review **runs** is not. -- **Review identity**: `none` (default) — every code-host write (the native review verdict, the `pair-review` publication) executes with the **session token**, i.e. the human or agent running the flow. Set to `app` (a GitHub App — recommended: it unlocks the Checks API and makes "who reviewed" auditable per-identity) or `bot-user` (a second machine account) to have those writes execute as a **dedicated review identity**, per [github-implementation.md](../../knowledge/guidelines/collaboration/project-management-tool/github-implementation.md) § Dedicated review identity (the model and the actor table are in [pr-states.md](../../knowledge/guidelines/collaboration/project-management-tool/pr-states.md)). Configured-but-broken is a **HALT**, never a silent fallback to the session user. It does **not** relax the 🔴 rule — and the exclusion is mechanical in two forms: an `app` identity is rejected by `pair-explicit-approval`'s account-type clause (`user.type == "User"`), while a `bot-user` identity **does** type as `"User"` and is rejected only by its login, so that form additionally **requires** the repository variable `REVIEW_IDENTITY_LOGIN` — unset, the identity is not healthy and the flow HALTs. Either way a `risk:red` PR still needs a second human account. A native `APPROVE` is submitted **only** where the adoption-gated light row authorizes it; every other approving verdict stays a comment-form review, so the identity never satisfies a host `required_approving_review_count` on the project's behalf. - **Coverage guardrail**: `enabled` — pair dogfoods its own capability: the [`Coverage guardrail` step](../../../.github/workflows/ci.yml) in CI sources [`coverage-gate.sh`](../../knowledge/assets/coverage-gate.sh), extracts the line-coverage % from each package's istanbul `coverage-summary.json`, and blocks a PR whose coverage drops below the human-committed baseline in [`tech/coverage-baseline.md`](./coverage-baseline.md) (maintaining/improving passes — not an absolute wall). The framework **default remains `disabled`** (the dataset template ships off); this line is pair's project-level opt-in only. See [coverage guardrail](../../knowledge/guidelines/infrastructure/cicd-strategy/tier-aware-pipeline.md#coverage-guardrail-opt-in-regression-gate-consumed-by-this-pipeline) + [config format](../../knowledge/assets/coverage-config-example.md); `/pair-capability-setup-gates` reads this flag before generating the pipeline. **Coverage baseline commit-back**: `disabled` — the separate, nested opt-in ratchet (#372, framework default also `disabled`): when `enabled`, a **push to the base branch** (never a PR run, never a fork) proposes a raised `baseline.` as a **bot pull request** from `chore/coverage-baseline-ratchet`, never a push to `main`, and requires a repo-scoped `COVERAGE_RATCHET_TOKEN` (`contents: write` + `pull requests: write`, no protection bypass) — without it the step warns and the gate's verdict is unchanged. It stays `disabled` here until story #234's branch protection is applied and that secret is provisioned (ADR-018 lands with that story, so it is not linked from here yet); see ADL [2026-07-30-coverage-ratchet-pr-not-push.md](../decision-log/2026-07-30-coverage-ratchet-pr-not-push.md). The step that runs it is the **shipped** KB asset `node .pair/knowledge/assets/coverage-ratchet.cjs` (ADR-023) — the same one an adopter's generated pipeline invokes, so this flag being `enabled` means the same thing here as anywhere else. - **Pair review required checks**: `pair-review` + `pair-explicit-approval` are the required status checks that make the judgment review unskippable (R5.7) and enforce the 🔴 explicit-human-approval rule (D10) — see [pr-states.md](../../knowledge/guidelines/collaboration/project-management-tool/pr-states.md) and [ADR-018](adr/adr-018-pr-state-flow-required-checks.md). Status on this repo: **not yet applied** — writing branch protection needs admin scope, so it is a deliberate human step; until applied, enforcement here is advisory (the documented degraded mode). **Ordering constraint** (applies in this order, or every merge stops): 1. provision the `pr-state:*` labels + add the `pair-explicit-approval` workflow (neither needs admin scope — this repo has not added the workflow yet, so the context does not report today); 2. confirm on a real PR that `pair-review` and `pair-explicit-approval` both report on the head commit, **and** that the approval context re-reports on that same head SHA after a review submission; 3. only then `PUT` the branch protection, keeping `enforce_admins` off until one PR has merged through it. The whole sequence (including the merge-block outcomes per tier) was executed on a throwaway repository — see `github-implementation.md` § "Verified on a throwaway repository" — so what remains here is applying it, not discovering whether it works. **This repo is single-maintainer**, so a 🔴 PR cannot satisfy `pair-explicit-approval` (GitHub rejects a self-approval): a second human reviewer account is a prerequisite for making that context required here — otherwise leave it out of the required list and keep the 🔴 rule advisory. The solo-maintainer alternative (a verified human approval token instead of a second account) is tracked as [#398](https://github.com/foomakers/pair/issues/398). **When the protection is written here, use the `checks` form with `app_id` pinned** for `pair-explicit-approval` (an unpinned status context is satisfiable by any push-access token, including the agent's); `pair-review` stays unpinned and is an anti-accident control, not an authorization control — see `github-implementation.md` § "What each context proves". -- **`format` required check**: `format` is the third context in the "declared but not yet required" set, alongside the two above — CI runs `pnpm format:check` on every pull request and on push to `main` (plus `workflow_dispatch`, the same manual escape hatch `ci.yml` carries), via its own [`format.yml`](../../../.github/workflows/format.yml) workflow (#413), published as the status context `format`. A **dedicated workflow, not a job in `ci.yml`**: that workflow's workflow-level `paths-ignore: ['.changeset/**']` is inherited by every job, so a `.changeset/**`-only PR would run no formatting check — trigger coverage is part of check coverage. The workflow's shape — triggers and filters, concurrency, permissions, the exact command, the scoped remedy, the checkout's inputs, the workflow/job/step key allow-lists and every other allow-list — is asserted by [`format-workflow-composition`](../../../packages/dev-tools/src/quality-gates/format-workflow-composition.ts) in `@pair/dev-tools`: that module's header is the rule inventory (one source, not restated here), and ADL [2026-09-01-workflow-guard-rejects-what-it-cannot-read.md](../decision-log/2026-09-01-workflow-guard-rejects-what-it-cannot-read.md) records that the guard PARSES the file with `yaml@2.8.2` (no spelling requirement: flow style, anchors, aliases, JSON steps and CRLF are read) and that a file the parser refuses is itself a problem. Enforced twice: by `pnpm test` (the `$TURBO_ROOT$` input, bullet above) and by `pnpm gate:composition` (`format-workflow:check`). Status on this repo: **not yet applied** as a required check — same pending admin-scope step, and the same ordering constraint, as the row above; until then it reports and is advisory. Unlike `pair-explicit-approval` it needs **no `app_id` pinning and no second human account**: it asserts a mechanical property of the tree, so it is an anti-accident control like `pair-review`, not an authorization control. - **Gate & tooling code:** a gate's logic lives in a tested module in its owning package (white-box unit tests); scripts/CLIs are thin entrypoints and a root gate delegates (`pnpm --filter `). Scripts are never unit-tested — CLI-level checks go to smoke tests. See ADL [2026-07-13-gate-tooling-code-in-tested-modules.md](../decision-log/2026-07-13-gate-tooling-code-in-tested-modules.md). Gate/tooling packages are organized by bounded context, not one package per tool family — a new tool family sharing an existing package's bounded context is a new folder there, not a new package. See [ADR-014](adr/adr-014-tool-package-boundary-by-bounded-context.md). +- **OS guarantees are tested against the OS.** The in-memory `FileSystemService` double is the default, but when the behaviour under test IS an atomicity or containment guarantee of the operating system (an exclusive create, an append, symlink containment), the module calls `node:fs` directly, stays a **leaf** with the primitive injected at its call site, and is tested against a real temporary directory — a double that cannot fail the way production fails proves nothing. See ADL [2026-08-30-atomicity-primitives-use-node-fs-directly.md](../decision-log/2026-08-30-atomicity-primitives-use-node-fs-directly.md). +- **A CLI flag carrying an OBSERVATION is empty-valid; a flag carrying INTENT is not.** Every `pair run` flag refuses an empty value, with one documented exception: `--card-tags`, which reports the labels a host trigger observed on a card. "No labels" is a real state of every board (and what `join(labels.*.name, ',')` renders for an unlabelled issue), so an empty value there is data — read as an empty tag list and skipped cleanly — while an empty item INSIDE the list still HALTs. See ADL [2026-08-30-empty-card-tags-is-an-observation-not-a-malformed-flag.md](../decision-log/2026-08-30-empty-card-tags-is-an-observation-not-a-malformed-flag.md). - **Conformance tests** (`packages/knowledge-hub/src/conformance/`): one test file per target KB artifact (a `SKILL.md`, guideline, or template), not per introducing story — a new story extends the matching file's `describe` block instead of adding a new story-named file. See ADL [2026-07-18-conformance-test-per-file-not-per-story.md](../decision-log/2026-07-18-conformance-test-per-file-not-per-story.md). - **Monorepo tooling gotchas** (e.g. `pnpm --filter` bypassing turbo's `dependsOn` graph on a fresh checkout): documented once, centrally, in `DEVELOPMENT.md`'s `Turbo Caching` section — affected packages' READMEs carry only a short pointer, not a full copy. See ADL [2026-07-18-workspace-gotcha-doc-placement.md](../decision-log/2026-07-18-workspace-gotcha-doc-placement.md). diff --git a/.pair/knowledge/guidelines/collaboration/automation/README.md b/.pair/knowledge/guidelines/collaboration/automation/README.md index 0f4d6515e..338e9f780 100644 --- a/.pair/knowledge/guidelines/collaboration/automation/README.md +++ b/.pair/knowledge/guidelines/collaboration/automation/README.md @@ -28,9 +28,9 @@ This framework does not cover: ## Directory Contents -**[automation-policy.md](automation-policy.md)** - `tech/automation.md` schema: the `## Eligibility` declaration that selects which cards may run unattended +**[automation-policy.md](automation-policy.md)** - `tech/automation.md` schema: the `## Eligibility` declaration that selects which cards may run unattended, and the `## Workflows` mapping that routes a tagged card to the workflow that runs on it -**[github-automation.md](github-automation.md)** - GitHub Actions and workflow automation strategies +**[github-automation.md](github-automation.md)** - GitHub Actions and workflow automation strategies, including the reference tag-driven dispatch trigger adapter **[azure-devops-automation.md](azure-devops-automation.md)** - Azure DevOps board rules, branch policies, and service hooks diff --git a/.pair/knowledge/guidelines/collaboration/automation/automation-policy.md b/.pair/knowledge/guidelines/collaboration/automation/automation-policy.md index 4da2ad68c..883cb79c2 100644 --- a/.pair/knowledge/guidelines/collaboration/automation/automation-policy.md +++ b/.pair/knowledge/guidelines/collaboration/automation/automation-policy.md @@ -2,7 +2,7 @@ How much of the delivery flow a project lets run **unattended** is a project decision, so it lives in an adoption file: the optional `.pair/adoption/tech/automation.md`. This guideline defines that file's schema. -It specifies six sections, landed across two stories and owned one at a time so two stories never claim the same lines of the same file: `## Eligibility` (#216) selects **which cards** an unattended run may pick up at all; `## Harness`/`## Model Policy` (#450) declare supported agent harnesses and per-tier model class; and `## Auto-Advance`, `## Stop Predicate`, `## Max Parallelism`, `## Audit Location` (#250) are the remaining ADR-017 §6 knobs — which tier may auto-merge, when a run stops, the parallel-batch ceiling, and where the audit trail is written. +It specifies seven sections, landed across several stories and owned one at a time so two stories never claim the same lines of the same file: `## Eligibility` (#216) selects **which cards** an unattended run may pick up at all; `## Workflows` (#217) maps **a tag to the workflow that runs on a card carrying it**, which is what makes automation opt-in per card; `## Harness`/`## Model Policy` (#450) declare supported agent harnesses and per-tier model class; and `## Auto-Advance`, `## Stop Predicate`, `## Max Parallelism`, `## Audit Location` (#250) are the remaining ADR-017 §6 knobs — which tier may auto-merge, when a run stops, the parallel-batch ceiling, and where the audit trail is written. **Eligibility selects `which cards`, never which gates.** The per-tier gate/approval policy already exists in [`quality-model.md`](../../quality-assurance/quality-model.md) §4 and is not restated here — auto-advance *enacts* that policy, it does not redefine it. Two sources of truth for the same rule is the failure mode this split exists to prevent. @@ -100,6 +100,7 @@ An **untagged** card — one carrying no `risk:*` label at all — never matches | Is the card Ready / in the right state? | The consumer's own selection rules (`pair-next`). Eligibility is a **label** predicate only; state and readiness gating stay where they already live | | Auto-advance switch, stop predicate, step defaults, `max_parallelism`, audit location | ADR-017 §6 — the four sections below (`## Auto-Advance`, `## Stop Predicate`, `## Max Parallelism`, `## Audit Location`), landed by the automation loop story (#250) | | Which label a card carries | `classify`, via the Tag Projection declaration in `tech/risk-matrix.md` | +| Which workflow runs on an eligible card | `## Workflows` below — the tag→workflow mapping (#217). Eligibility selects, the mapping routes; neither answers the other's question | ## Auto-Advance — which tiers may push/merge unattended @@ -198,6 +199,106 @@ automation/loop-audit.md If the resolved path cannot be created or written, `pair-loop` **MUST HALT the run** rather than proceed unaudited: an unattended run with no audit trail is not an acceptable degraded mode (ADR-017 §6). +## Workflows — which workflow each tag routes to + +The **tag→workflow mapping** (#217, R4.4): the declaration that makes automation opt-in **per card** instead of per run. `## Eligibility` above answers *which cards an unattended run may pick up at all*; this section answers *what runs on a card once a trigger fires on it*, keyed by a tag the card carries. + +```markdown +## Workflows + +auto-dev ⇒ pair-loop +auto-plan ⇒ pair-process-plan-tasks +Precedence: auto-dev, auto-plan +``` + +- **One entry per line, ``** — the same `⇒` (U+21D2) `## Stop Predicate` uses, and only that one. An ASCII `=>` is a **HALT** naming the documented spelling, so the same file cannot mean different things to two consumers. +- **The tag is an OPAQUE routing key** (D18). It is matched against the card's labels with the plain **string equality** `## Eligibility` already uses — no tier arithmetic, no family knowledge, and **no classification criteria anywhere in the routing code**: tags are produced by `classify`, and a workflow only ever *reads* them. That property is grep-verifiable, and it is meant to be. +- **The workflow is a skill name** — the entry point of a composition of existing skills, never a bespoke engine and never a merit rule. Two conditions, both required: it must be **installed**, and it must be one the dispatcher can hand the dispatched card to — the set named in *"The workflows a mapping can name"* below. A skill that is installed but outside that catalog is **refused**, and the refusal stops dispatch for the whole board (see the routing-time HALTs), so this bullet is not the whole rule: read it together with the catalog section. +- **`Precedence: , , …`** — optional, at most one line, first listed wins. It resolves a card carrying **more than one** mapped tag, and nothing else. + +### Untagged ⇒ never. That is the whole opt-in boundary. + +A card carrying **no mapped tag never runs**. There is no default workflow, no "fall back to the develop workflow", no implicit route for an unmapped card — a consumer **MUST** skip it and log the skip. The absence of a route is the authorization decision, so widening it is not a convenience: it is the difference between automation on the cards a team named and automation on the backlog. + +### Absent section ⇒ no workflow is available + +`## Workflows` absent (or the whole optional file absent) ⇒ **no mapping is declared**: nothing can be routed. A dispatch **MUST** report `no mapping declared`, naming the file, and **exit cleanly** — automation is opt-in (D21), so a project that never wrote this section has simply not opted in, and that is never an error and never a default workflow. + +**Absent section ≠ empty section**, exactly as under `## Eligibility`: a heading with no entry is a **half-written declaration** ⇒ HALT. + +### Eligibility is applied BEFORE routing + +The order is normative. A card that does not match `## Eligibility` is **skipped before its tags are looked at at all**, and the skip is **logged**. Routing an ineligible card and relying on a later gate to stop it would put the eligibility filter — the one declaration that keeps business-critical work out of an unattended pipeline — after the decision it exists to bound. + +### Not a routable mapping ⇒ HALT + +At **read** time, a consumer **MUST HALT** with an adoption-fix message naming the file and the offending value when: + +1. the section is present with **no entry line** (a half-written declaration); +2. a line matches **neither** `` **nor** `Precedence: , …`; +3. an entry uses `=>` instead of `⇒`; +4. the **same tag** is declared twice — one card would route to two workflows, and picking one silently is what this HALT prevents; +5. a **tag** is not usable as a label: longer than the host's label-name cap (**50 characters** on GitHub; another tracker applies its own), carrying a comma or a standalone `AND`/`OR`/`NOT`, opening with a markdown block marker, or containing a character that could turn it into a command fragment once inlined in an agent prompt. These are `## Eligibility`'s own triggers 3–5 plus the content MUST, applied to the same kind of value for the same reasons — one rule set, not a second one; +6. a **workflow name** is not a plain identifier (it is spliced into an agent invocation *and* used as a path segment when probing whether the skill is installed); +7. there is **more than one `Precedence:` line**, the line is empty, it repeats a tag, or it names a tag no entry declares — a precedence naming an undeclared tag is dead configuration that reads as a working tie-break; +8. the file carries **more than one `## Workflows` heading** — counted as rendered markdown at level 2, so an occurrence inside a fenced code block is not one. + +At **routing** time — the two rules that need a board and an installed skill set, so they cannot be answered from the file alone: + +- **a mapped tag whose workflow is not installed ⇒ HALT** with an adoption-fix message naming the tag, the workflow and the file. Never a silent fall back to another workflow: running a *different* workflow than the one declared is the outcome no operator can debug. The check runs **before** eligibility and routing, so this HALT stops dispatch for **every** card — including cards that are ineligible or carry no mapped tag at all — not only the cards carrying the offending tag. One broken line is broken configuration for the whole board, and that is the point: making the failure surface only on whichever card happens to carry that tag would make it depend on which trigger fired first; +- **a card carrying two or more mapped tags with no `Precedence:` line — or with none of those tags listed in it — ⇒ HALT**. A silent choice between two declared workflows is precisely what the precedence line exists to prevent, so its absence is a question for a maintainer, not a tie for a consumer to break. + +### One run per card — the concurrency guard + +A trigger fires on card metadata, and metadata changes in **bursts** (a label added, removed, re-added; a re-run of the same host job). A consumer **MUST** take an **exclusive per-card lock** before it dispatches and release it when the run ends; a second dispatch for a card whose lock is held is **skipped and logged**, never queued behind the first. Two agent runs on one card is the failure mode this guard exists for: they would race on the same branch, the same PR and the same board state. + +**The lock is scoped to ONE working area** (ADR-024): it stops two dispatches that share `working_path` — a persistent daemon, a long-lived runner — and it cannot see a holder on another machine or in another fresh checkout. A host whose jobs get an ephemeral workspace **MUST** put every path that dispatches a card into one host-side concurrency group, because there the group is the only cross-job guard there is. + +**A lock has no timeout and nothing reaps it.** A run killed by a signal, an OOM kill or a job timeout leaves the lock behind, and automation is then silently off for that card: every later trigger skips and exits cleanly. A consumer **MUST** therefore report, in the skip, *where* the lock is and *how long* it has been held — the two facts that separate a healthy burst from a stale lock — and the operator surface **MUST** document clearing it. + +### The audit trail — and where host credentials are not + +Every dispatch decision — **start**, **skip**, **end** — is appended to the run's `## Audit Location` file. The **start** record, and **only** the start record, is *also* emitted on stdout as a single `DISPATCH-RECORD:` line, so the **trigger's host adapter** — the thin, per-host piece that already holds the credentials the trigger runs under — can post it as a comment on the card. A skip and an end stay in the file: a card that gets a comment for every unmapped label edit is unreadable within a day, and an end comment doubles the noise for a fact the trail already holds. The dispatcher core stays **host-agnostic**: it reads tags it was handed, resolves a workflow and writes a file, and never holds a tracker token. Adding a host is a new adapter, never a change to the routing core. + +### The workflows a mapping can name + +A workflow is a **skill that already exists** — the entry point of a composition, so there is no bespoke engine to write and mapping a tag costs one line of adoption. **This table is the mappable set**, not an illustrative sample of it: a mapping may name one of these and nothing else, and a skill outside it is refused even when installed. They are the compositions pair ships, and they are the reason the mapping needs no vocabulary of its own. + +| Workflow | What a card routed to it gets | A tag teams usually map to it | How the dispatched card reaches it | +| --- | --- | --- | --- | +| `pair-loop` | the delivery loop — selects the card, implements it, opens the PR, drives the review/fix rounds, and stops at a review-approved PR (it never merges outside `## Auto-Advance`) | `auto-dev` | `--root ` | +| `pair-process-plan-tasks` | a refined story broken into implementation tasks, with the dependency graph and the AC-coverage table written back onto the card | `auto-plan` | `--story ` | + +**A mapping may only name a workflow in that table, and the last column is why.** The dispatched card is the whole subject of the run, and it arrives as an **argument** — under the name that workflow's own `## Arguments` table declares, borrowed and never invented (D18). The two above spell it differently, and the difference is not cosmetic: `pair-process-plan-tasks` states that **when its `$story` is absent it selects the highest-priority story on the board itself**. So a card handed to it under a name it does not declare is a card it never sees — the workflow runs, on a *different* card, while the audit trail and the on-issue `DISPATCH-RECORD:` both name the card that was tagged. A consumer therefore **MUST HALT** on a mapped workflow this table does not list, with an adoption-fix message, rather than dispatch a run it cannot scope — the same fail-fast, whole-board check an uninstalled workflow already gets, and for the same reason: a run nobody is watching must never pick its own subject. + +**Being scopable is not enough to be mappable.** A consumer may well know how some other skill spells its scope — `pair-next` takes `--root`, and driving it by hand with one is perfectly legitimate — and that is still not a licence to route a card to it. A dispatch takes the card's exclusive lock, writes an `event=start` audit line and emits the `DISPATCH-RECORD:` line the host adapter posts as a comment; a skill that only *reports* changes nothing, so all a team gets is a card claiming work that never happened and a lock nobody needed. Widening the set is a change to this table, made deliberately, with the "what a card routed to it gets" and "how the dispatched card reaches it" columns filled in — never a side effect of a consumer happening to know an argument name. + +#### A workflow that needs a human in the room is not mappable + +A dispatch runs with **nobody watching**, under an autonomy posture the operator opted into once, for as long as the per-iteration timeout allows. So the table above admits only workflows that can **reach a terminal outcome without an interlocutor**. A workflow whose steps require an explicit human decision — a `Human-judgment gate`, an alignment sync that ends only on an explicit "yes" — has exactly two outcomes when it is dispatched, and both are worse than not running: + +- it **stalls** on a question no one answers, until the per-iteration timeout kills it, holding the card's exclusive lock for the whole window while the card carries a public comment saying a run started; or +- the agent, having no one to ask, **supplies its own approval** and drives the card past the gate — which is the authorization control the gate exists to be, satisfied by the party it exists to constrain. + +`pair-process-refine-story` is the concrete exclusion and the reason this rule is written down: it is the single Draft→Ready path, and its own SKILL.md calls phase 0 "the R3.11 AI↔human alignment gate — a prerequisite, not optional", adds three per-step `Human-judgment gate`s, and closes with "what is never skipped is explicit human alignment before the story reaches `Ready`". It is a **hand-driven** workflow (`/pair-process-refine-story --story `, or the refine batch), not a tag-driven one. Refinement is where a human belongs; the mapping is for the work that follows it. + +```markdown +## Workflows + +auto-plan ⇒ pair-process-plan-tasks +auto-dev ⇒ pair-loop +Precedence: auto-plan, auto-dev +``` + +Two properties of that example are worth stating, because both are load-bearing rather than stylistic: + +- **The precedence line is what makes the pair safe.** A card that has just been broken into tasks often still carries `auto-plan` when `auto-dev` is added; without the line, that card is a HALT the moment a trigger fires on it. Declaring `auto-plan` first is not a preference — it is the answer to a question the dispatcher refuses to answer for you. +- **A workflow is never mapped to two tags to mean two intensities of it.** Tags carry no merit (D18), so `auto-dev-fast ⇒ pair-loop` and `auto-dev ⇒ pair-loop` route identically; what varies a run's behaviour is the policy above (`## Eligibility`, `## Stop Predicate`, `## Max Parallelism`), never the tag that routed it. + +### What fires the dispatch — the per-host adapter + +Nothing in this file starts a run. A **trigger** does: a thin, per-host piece that observes a card's labels changing and calls the entry point with what it already holds, `pair-cli run --card --card-tags `. It is the component that carries the tracker credentials, and the one that posts the `DISPATCH-RECORD:` line back onto the card. The reference implementation — a GitHub Actions job firing on `issues: [labeled]` — is in [github-automation.md](github-automation.md); a host with webhooks and a job runner (Azure DevOps service hooks, a Jira automation rule) is the same shape against a different API, and adding one never touches the routing core. + ## Harness and Model Policy A second, independent section of the same file — disjoint from `## Eligibility` above (which cards run unattended) and from `## Auto-Advance` / `## Stop Predicate` / `## Max Parallelism` / `## Audit Location` (the rest-of-file schema ADR-017 §6/#250 lands). This section answers two different questions: **which agent harnesses this project supports**, and **which model class each risk tier gets**. `/pair-capability-setup-harness` reads exactly these two declarations; the [agent-harness framework](../../technical-standards/ai-development/agent-harness/README.md) documents what each harness value means. diff --git a/.pair/knowledge/guidelines/collaboration/automation/github-automation.md b/.pair/knowledge/guidelines/collaboration/automation/github-automation.md index e377f500a..27d43d27b 100644 --- a/.pair/knowledge/guidelines/collaboration/automation/github-automation.md +++ b/.pair/knowledge/guidelines/collaboration/automation/github-automation.md @@ -195,6 +195,120 @@ jobs: - Integration with external tools and notification systems - Advanced reporting and analytics automation +## Tag-Driven Dispatch — the reference trigger adapter + +The **trigger** for tag-driven workflows: the thin, host-specific piece that turns "a label was added to an issue" into one call to pair's entry point. Everything it decides is decided here; everything it *routes* is decided by `## Workflows` in `tech/automation.md` (schema: [automation-policy.md](automation-policy.md)). The adapter is deliberately small — five steps, three of them setup, and no logic of its own — because that is what keeps every host on the same routing core. + +**The runner does not ship `pair-cli`.** `ubuntu-latest` has never heard of it, so the job installs the CLI itself: without that step `pair-cli run` is `command not found`, the step exits 127, the job goes red and nothing is ever routed or audited. The **engine** the run spawns (`claude`, `pi`, `opencode`) and its credentials are the adopter's own step — the block below installs the driver, not the agent. + +### The workflow + +```yaml +name: pair dispatch +on: + issues: + types: [labeled] + +# One in-flight job per issue. On EPHEMERAL runners this group IS the cross-job guard, and +# `cancel-in-progress: false` is what makes it one: every job checks out a fresh workspace, +# so the per-card lock `pair-cli run` takes lives in a working area no other job can see, and it +# can never observe a holder from another runner (ADR-024: the lock is filesystem-local). +# A second trigger declared OUTSIDE this group — a `workflow_dispatch` button, an +# `issue_comment` job — therefore starts a second agent on the same card, the same branch +# and the same PR. Put every path that dispatches a card into THIS group. +# The per-card lock is the guard within ONE working area: a persistent daemon box, where the +# bursts it stops are real and the host has no concurrency group at all. +concurrency: + group: pair-dispatch-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + dispatch: + runs-on: ubuntu-latest + # The credentials live HERE, at the adapter — the narrowest set that lets it read the + # issue it was handed and post one comment on it. `pair-cli run` itself is given none. + permissions: + issues: write + contents: read + steps: + - uses: actions/checkout@v4 + + # `pair-cli` is NOT on a hosted runner. Without these two steps the next one is + # `bash: pair-cli: command not found` (exit 127) on every labeled event — a red job, + # nothing routed, nothing audited. Pin the version you adopted rather than + # `@latest` if you want the trigger to be reproducible. + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install the pair CLI + shell: bash + run: npm install -g @foomakers/pair-cli + + - name: Dispatch the card + id: dispatch + # DECLARED, not defaulted. GitHub's implicit shell is `bash -e {0}` — WITHOUT + # `pipefail` — so the pipeline below would report `tee`'s status and a HALT inside + # `pair-cli run` (an uninstalled workflow, an undecidable multi-tag card) would land as a + # green tick. `shell: bash` is what makes GitHub run the step with `-eo pipefail`. + shell: bash + env: + # The labels the trigger ALREADY observed — passed as data, never re-fetched. An + # adapter that queries the API for them is the tracker client the driver exists + # without. Through the environment, not string-interpolated into the command line. + CARD_TAGS: ${{ join(github.event.issue.labels.*.name, ',') }} + CARD: ${{ github.event.issue.number }} + run: | + pair-cli run --card "$CARD" --card-tags "$CARD_TAGS" --autonomous \ + | tee dispatch.log + + - name: Record the run on the card + if: always() + shell: bash + env: + GH_TOKEN: ${{ github.token }} + CARD: ${{ github.event.issue.number }} + run: | + # The driver PRINTS this line and never posts it: posting is the adapter's job, + # because the adapter is what holds a tracker token. No line ⇒ nothing was + # dispatched (untagged, unmapped or ineligible card) ⇒ nothing to comment. + record="$(grep '^DISPATCH-RECORD:' dispatch.log || true)" + # `if`, not `[ -n "$record" ] && …`: under `-e` a trailing compound whose test is + # false exits 1, so the silent case — the untagged issue this feature promises + # costs nothing — would fail the step and notify a human on every unmapped label + # edit on the board. + if [ -n "$record" ]; then + gh issue comment "$CARD" --body "$record" + fi +``` + +### What the adapter does and does not decide + +| Question | Answered by | +| --- | --- | +| Did anything happen on a card? | the host trigger (`types: [labeled]`) | +| Which workflow runs on it? | `## Workflows` in `tech/automation.md` — never the adapter, never a job condition | +| May this card run at all? | `## Eligibility`, applied by the dispatcher *before* routing | +| Is another run already on this card? | the per-card lock inside `pair-cli run` | +| Who tells the humans? | the adapter, by posting the `DISPATCH-RECORD:` line | + +**Only a run START is ever posted on the card.** `DISPATCH-RECORD:` is emitted for the `start` event and for nothing else — a skip and an end are appended to the audit file only. That is deliberate and it is the whole reason the comment step needs no filter of its own: a board where every unmapped label edit posted a "nothing happened" comment would be unreadable within a day, and an `end` comment would double every run's noise for a fact the audit trail already holds. An adapter that wants the end on the card reads it from `## Audit Location`; it must not re-derive it from the exit status. + +A job `if:` that pre-filters on a label is the one thing worth resisting: it duplicates the mapping in a second place, in a language the dispatcher cannot read, and the two drift on the day someone renames a tag in adoption. Let every labeled event through and let the routing core skip what it should skip — a skip is cheap, reported, and appended to the audit trail. + +### Untagged is not a case the adapter has to handle + +An issue carrying no mapped tag **runs nothing**: the dispatcher reports the skip and exits `0`, and with no `DISPATCH-RECORD:` line the comment step posts nothing. That includes the **unlabelled** issue, where `join(github.event.issue.labels.*.name, ',')` renders an empty string: `--card-tags` reads an empty value as the observation "this card carries no labels", so the adapter needs no guard clause and no conditional call — passing what the trigger saw is always correct. That is the opt-in boundary of the whole feature, and it lives in the routing core precisely so that no adapter can widen it by accident. The same holds when `tech/automation.md` declares no `## Workflows` section at all — the run reports `no mapping declared` and exits cleanly. + +### Before wiring the trigger + +- **Run it once by hand**, on a card you tagged deliberately: `pair-cli run --card --card-tags "" --dry-run` prints the route, the perimeter and the policy, and spawns nothing. +- **Provision `pair-cli` and the engine on the runner.** The job above installs the CLI; the **engine** it spawns (`claude`, `pi`, `opencode`) and that engine's credentials are yours to add. Neither is present on a hosted runner by default, and a missing binary is `command not found` — a red job, with nothing routed and nothing written to the audit file. +- **Scope the token to the repository the cards live in.** The adapter can only ever post where its token reaches; the engine credentials the run itself needs are a separate, and usually much broader, concern. +- **Watch the audit file** (`## Audit Location`) for the first few cycles: every start, skip and end is there, including the ones the card never shows. +- **Check the mapping resolves before the first trigger fires.** A `## Workflows` entry naming a workflow nobody installed — or one the dispatcher cannot hand the card to, i.e. anything outside the [catalog](automation-policy.md#the-workflows-a-mapping-can-name) — HALTs the dispatch *before* eligibility and routing, so it stops **every** card on the board, not only cards carrying that tag. That is the intended blast radius — a broken mapping is broken for everyone, and finding out only on the one card that happens to carry the tag would make the failure depend on which trigger fired first — but it means the dry run above is a check on the whole board, not on one card. +- **Know how to clear a stale lock.** The per-card lock is a directory (`/automation/locks//`) with no timeout and nothing to reap it: a run killed by SIGKILL, an OOM kill or a job timeout leaves it behind, and every later trigger on that card then skips with `run-in-progress` and exits `0` — automation silently off for that one card. The skip line prints the directory and how long it has been held; when no run is alive, `rm -rf` that directory to clear it. On ephemeral runners the workspace is discarded with the job, so this is a **persistent daemon** concern. + ## Implementation Guidelines ### Setup Process @@ -286,12 +400,4 @@ jobs: - Approval workflows and sign-off procedures - Compliance verification and audit trail management -This GitHub automation framework provides comprehensive automation capabilities that integrate seamlessly with development workflows while maintaining visibility, control, and reliability for team collaboration and project management.Automation - -## Overview - -This document outlines automation strategies for GitHub-based collaboration workflows. - -## TODO - -This document needs to be completed with GitHub automation guidelines. +This GitHub automation framework provides comprehensive automation capabilities that integrate seamlessly with development workflows while maintaining visibility, control, and reliability for team collaboration and project management. diff --git a/.pair/llms.txt b/.pair/llms.txt index 1e566e715..b6de24dbe 100644 --- a/.pair/llms.txt +++ b/.pair/llms.txt @@ -4,9 +4,8 @@ ## Adoption — Product -- [Product Requirements Document (PRD)](.pair/adoption/product/PRD.md) - [Context Map](.pair/adoption/product/context-map.md) -- [Subdomain Catalog Index](.pair/adoption/product/subdomain/README.md) +- [Product Requirements Document (PRD)](.pair/adoption/product/PRD.md) - [Adoption & Guidelines (Supporting Subdomain)](.pair/adoption/product/subdomain/adoption-guidelines.md) - [Code & Documentation Generation (Core Subdomain)](.pair/adoption/product/subdomain/code-documentation-generation.md) - [Collaborative Workflow — Context](.pair/adoption/product/subdomain/collaborative-workflow.context.md) @@ -14,10 +13,10 @@ - [Development Tooling Standards (Generic Subdomain)](.pair/adoption/product/subdomain/development-tooling-standards.md) - [How To Knowledge (Supporting Subdomain)](.pair/adoption/product/subdomain/how-to-knowledge.md) - [Integration & Process Standardization (Supporting Subdomain)](.pair/adoption/product/subdomain/integration-process-standardization.md) +- [Subdomain Catalog Index](.pair/adoption/product/subdomain/README.md) ## Adoption — Tech -- [📋 Adopted Standards & Practices](.pair/adoption/tech/README.md) - [ADR-001: TTY Detection Pattern for CLI UX](.pair/adoption/tech/adr/adr-001-tty-detection-pattern.md) - [ADR-002: HTTP Range Requests for Download Resume](.pair/adoption/tech/adr/adr-002-http-range-resume.md) - [ADR-003: SHA256 Checksum Validation for File Integrity](.pair/adoption/tech/adr/adr-003-checksum-validation.md) @@ -42,16 +41,16 @@ - [ADR-021: Fan-out is one capability with three realizations — in-harness, external driver, degraded](.pair/adoption/tech/adr/adr-021-fan-out-three-realizations.md) - [ADR-022: The coverage-baseline ratchet is EXPOSED through the published CLI, not ported to a shipped shell asset](.pair/adoption/tech/adr/adr-022-coverage-ratchet-exposed-through-the-cli.md) - [ADR-023: The coverage-baseline ratchet ships as a GENERATED KB asset, not as a CLI command](.pair/adoption/tech/adr/adr-023-coverage-ratchet-ships-as-a-generated-kb-asset.md) -- [ADR-024: Delivery phases are skills; the batch workflow only coordinates](.pair/adoption/tech/adr/adr-024-delivery-phases-are-skills.md) -- [ADR-025: The unit a process profile configures is the STEP, never one of its representations](.pair/adoption/tech/adr/adr-025-process-profile-unit-is-the-step.md) +- [ADR-024: Tag-driven dispatch — the mapping is adoption, the routing core is host-agnostic, the on-issue record belongs to the host adapter](.pair/adoption/tech/adr/adr-024-tag-driven-dispatch-agnostic-core-host-adapter.md) - [Architecture](.pair/adoption/tech/architecture.md) - [Automation Policy — this project's delta](.pair/adoption/tech/automation.md) -- [Bounded Context Catalog (Grouped)](.pair/adoption/tech/boundedcontext/README.md) - [Development Collaboration Context](.pair/adoption/tech/boundedcontext/development-collaboration.md) - [Integration & Process Standardization Context](.pair/adoption/tech/boundedcontext/integration-process-standardization.md) - [Knowledge & Standards Context](.pair/adoption/tech/boundedcontext/knowledge-standards.md) +- [Bounded Context Catalog (Grouped)](.pair/adoption/tech/boundedcontext/README.md) - [`tech/coverage-baseline.md` — pair coverage guardrail config](.pair/adoption/tech/coverage-baseline.md) - [Infrastructure](.pair/adoption/tech/infrastructure.md) +- [📋 Adopted Standards & Practices](.pair/adoption/tech/README.md) - [`tech/risk-matrix.md`](.pair/adoption/tech/risk-matrix.md) - [Tech Stack](.pair/adoption/tech/tech-stack.md) - [UX/UI](.pair/adoption/tech/ux-ui.md) @@ -126,40 +125,10 @@ - [Decision: a `pair.config.json` schema guard stays in `#config` and takes its vocabulary INJECTED, never imported from the command layer](.pair/adoption/decision-log/2026-08-24-config-schema-guards-take-their-vocabulary-injected.md) - [Decision: the CLI invocation name is `pair-cli`, not `pair`](.pair/adoption/decision-log/2026-08-25-cli-invocation-canonical-name-is-pair-cli.md) - [Decision: Post-merge cleanup covers local branches and worktrees; PR analyses retire at merge](.pair/adoption/decision-log/2026-08-25-post-merge-cleanup-covers-local-branches-pr-analyses-retire-at-merge.md) -- [Decision: a config reader detects the KEY loosely and accepts the VALUE strictly — and a shipped mirror is a governed copy](.pair/adoption/decision-log/2026-08-28-a-config-reader-detects-loosely-and-accepts-strictly.md) -- [Decision: a convention's MARKER belongs to the entrypoint; its POINTER may be disclosed to a sibling](.pair/adoption/decision-log/2026-08-28-a-conventions-marker-is-the-entrypoints-its-pointer-may-be-disclosed.md) -- [Decision: The breakdown-to-task feedback loop is one mechanism owned by /pair-process-implement, batched per invocation](.pair/adoption/decision-log/2026-08-28-task-progress-feedback-is-one-mechanism-owned-by-implement.md) - [Decision: tier 1's `$approval` posture is unconditional, and tier 1 has no declaring composition site yet](.pair/adoption/decision-log/2026-08-28-tier1-approval-posture-is-unconditional-and-has-no-declaring-composition-site-yet.md) -- [Decision: Business impact gets an opt-in `trivial-diff` override — a docs-only or comment-only change resolves green whatever subdomain it lives in](.pair/adoption/decision-log/2026-08-30-business-impact-reads-what-a-trivial-change-does-not-where-it-lives.md) -- [Analysis Log: Docs-site journey-first audit — which sections lead with the problem, which lead with the config table](.pair/adoption/decision-log/2026-08-30-docs-site-journey-first-audit.md) -- [Decision: the staleness gate reads the BINARY, and the `pair-cli` rename follows the gate's reach, not the file list](.pair/adoption/decision-log/2026-08-30-docs-staleness-invocation-rule-and-repo-wide-pair-cli-sweep.md) +- [Decision: the two atomicity primitives (exclusive create, append) use `node:fs` directly, in leaf modules tested against a real temporary directory](.pair/adoption/decision-log/2026-08-30-atomicity-primitives-use-node-fs-directly.md) +- [Decision: an empty `--card-tags` means "this card carries no labels", not a malformed flag](.pair/adoption/decision-log/2026-08-30-empty-card-tags-is-an-observation-not-a-malformed-flag.md) - [Decision: Review re-checks use an immutable baseline and prove provisioned artifacts](.pair/adoption/decision-log/2026-08-31-review-baseline-and-provisioned-artifact-contract.md) -- [Decision: a generated artifact that is tracked and byte-compared is byte-reproducible across environments — fixed entry order, pinned line endings](.pair/adoption/decision-log/2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md) -- [Decision: a quality gate imports the generator it checks by SOURCE path, and the generator hands it a read-only file-system slice](.pair/adoption/decision-log/2026-09-01-a-gate-imports-its-generator-by-source-and-gets-a-read-only-slice.md) -- [Decision: External boundary proof prevents false equivalence](.pair/adoption/decision-log/2026-09-01-external-boundary-proof-prevents-false-equivalence.md) -- [Decision: a workflow guard reads a quoted `echo` argument as data, never as a command](.pair/adoption/decision-log/2026-09-01-quoted-echo-arguments-are-data-not-commands.md) -- [Decision: a repo-wide guard is enforced by a `$TURBO_ROOT$` cache input, not necessarily by a thin CLI + root gate step](.pair/adoption/decision-log/2026-09-01-repo-wide-guard-enforced-by-turbo-root-input.md) -- [Decision: Review contract inventory prevents serial findings](.pair/adoption/decision-log/2026-09-01-review-contract-inventory-prevents-serial-findings.md) -- [Decision: the format-workflow guard PARSES `format.yml` with `yaml@2.8.2`, and every rule is an allow-list over the parsed document](.pair/adoption/decision-log/2026-09-01-workflow-guard-rejects-what-it-cannot-read.md) -- [Decision: a gate's remedy is a command its audience can run, and the remedy refuses the states its own caution warns about](.pair/adoption/decision-log/2026-09-03-a-gate-names-a-remedy-it-can-run.md) -- [Decision: the review severity floor defaults to Minor — Questions are carried, never fixed](.pair/adoption/decision-log/2026-09-03-the-review-severity-floor-defaults-to-minor.md) -- [Decision: The docs-site deploy build routes through turbo](.pair/adoption/decision-log/2026-09-08-deploy-build-routes-through-turbo.md) -- [Decision: Repo citation anchors are checked against github.com's own slugs, offline](.pair/adoption/decision-log/2026-09-08-repo-citation-anchors-are-githubs-own-slugs.md) -- [Decision: Repo citations in the docs are gated through the site's own MDX compiler](.pair/adoption/decision-log/2026-09-08-repo-citations-are-gated-through-the-site-compiler.md) -- [Decision: Use a deterministic code canary for delivery workflow validation](.pair/adoption/decision-log/2026-09-09-deterministic-code-canary-for-delivery-workflow.md) -- [Decision: A demonstrably new scope proposal is queued and never absorbed or carded automatically — the maintainer alone chooses ignore, extend-current-card or new-card](.pair/adoption/decision-log/2026-09-10-scope-proposals-are-a-human-decision.md) -- [Decision: An approved `extend-current-card` delta is refused outright when the card speaks none of the recognized AC formats — fail-closed, never appended](.pair/adoption/decision-log/2026-09-10-unknown-ac-card-format-fails-closed.md) -- [Decision: custody never infers a breach from what it cannot see](.pair/adoption/decision-log/2026-09-12-custody-never-infers-a-breach-from-what-it-cannot-see.md) -- [Decision: rollback takes a head, its notes live in the handoff, and nobody deletes them](.pair/adoption/decision-log/2026-09-12-rollback-notes-are-derived-from-handoffs.md) -- [Decision: PR-comment marker matching stays author-blind — the planting risk is accepted, recorded, with an exit path](.pair/adoption/decision-log/2026-09-13-pr-comment-marker-matching-stays-author-blind.md) -- [Decision: a scope decision is keyed by the proposal's id and type — never its wording — and the cycle discovers it on the PR before asking again](.pair/adoption/decision-log/2026-09-13-scope-decision-identity-is-the-proposal-id.md) -- [Decision: tech debt — the engine's card transport is Claude Code + GitHub (`gh`) for now, recorded with its exit path](.pair/adoption/decision-log/2026-09-13-tech-debt-card-transport-is-claude-code-plus-github-for-now.md) -- [Decision: the final reviewer of a cycle concludes the required `pair-review` check and the `pr-state:*` label — merge stays outside the engine](.pair/adoption/decision-log/2026-09-13-the-final-reviewer-concludes-the-pair-review-check-and-the-pr-state-label.md) -- [Decision: when no host runtime is present, the final reviewer runs `finalize` — the synthesis is still the script's, never the reviewer's prose](.pair/adoption/decision-log/2026-09-13-the-final-reviewer-finalizes-when-no-host-runtime-is-present.md) -- [Decision: T-8 is reduced — the paired 2.0.0 vs 4.0.0 measurement is not run, and the full-cycle canary evidence stands in its place](.pair/adoption/decision-log/2026-09-13-the-paired-baseline-measurement-is-not-run.md) -- [Decision: the run-directory lock records its owner, breaks a dead writer's lock and refuses a stale live one out loud](.pair/adoption/decision-log/2026-09-13-the-run-directory-lock-has-an-owner-and-a-staleness-rule.md) -- [Decision: the scope-baseline hash is published to the reviewer — a consumer-only hash is a question nobody can answer](.pair/adoption/decision-log/2026-09-13-the-scope-baseline-hash-has-a-producer-the-reviewer-can-run.md) -- [Decision: the scope-decision principal is read from adoption — never a login literal in shipped code](.pair/adoption/decision-log/2026-09-13-the-scope-decision-principal-is-read-from-adoption.md) ## How-To Guides @@ -175,9 +144,6 @@ ## Guidelines -- [📚 Technical Guidelines Knowledge Base](.pair/knowledge/guidelines/README.md) -- [Architecture](.pair/knowledge/guidelines/architecture/README.md) -- [Architectural Patterns](.pair/knowledge/guidelines/architecture/architectural-patterns/README.md) - [Clean Architecture Pattern](.pair/knowledge/guidelines/architecture/architectural-patterns/clean-architecture.md) - [Continuous Architecture Pattern](.pair/knowledge/guidelines/architecture/architectural-patterns/continuous-architecture.md) - [CQRS (Command Query Responsibility Segregation)](.pair/knowledge/guidelines/architecture/architectural-patterns/cqrs.md) @@ -185,106 +151,106 @@ - [Event Sourcing Pattern](.pair/knowledge/guidelines/architecture/architectural-patterns/event-sourcing.md) - [Hexagonal Architecture (Ports and Adapters)](.pair/knowledge/guidelines/architecture/architectural-patterns/hexagonal.md) - [Layered Architecture Pattern](.pair/knowledge/guidelines/architecture/architectural-patterns/layer-architecture.md) +- [Architectural Patterns](.pair/knowledge/guidelines/architecture/architectural-patterns/README.md) - [Transaction Script Pattern](.pair/knowledge/guidelines/architecture/architectural-patterns/transaction-script.md) -- [Decision Frameworks](.pair/knowledge/guidelines/architecture/decision-frameworks/README.md) - [📋 Decision Records Practice (Level 2)](.pair/knowledge/guidelines/architecture/decision-frameworks/adr-process.md) - [Decision Tracking Framework](.pair/knowledge/guidelines/architecture/decision-frameworks/decision-tracking.md) - [Evolution Strategy Framework](.pair/knowledge/guidelines/architecture/decision-frameworks/evolution-strategy.md) +- [Decision Frameworks](.pair/knowledge/guidelines/architecture/decision-frameworks/README.md) - [Technology Selection Framework](.pair/knowledge/guidelines/architecture/decision-frameworks/technology-selection.md) -- [Deployment Architecture Patterns](.pair/knowledge/guidelines/architecture/deployment-architectures/README.md) - [Desktop Self-Hosted Deployment](.pair/knowledge/guidelines/architecture/deployment-architectures/desktop-self-hosted.md) - [Hybrid Architecture](.pair/knowledge/guidelines/architecture/deployment-architectures/hybrid.md) - [Microservices Architecture](.pair/knowledge/guidelines/architecture/deployment-architectures/microservices.md) - [Modular Monolith Architecture](.pair/knowledge/guidelines/architecture/deployment-architectures/modular-monolith.md) +- [Deployment Architecture Patterns](.pair/knowledge/guidelines/architecture/deployment-architectures/README.md) - [Serverless Architecture](.pair/knowledge/guidelines/architecture/deployment-architectures/serverless.md) - [Structured Monolith Architecture](.pair/knowledge/guidelines/architecture/deployment-architectures/structured-monolith.md) -- [Design Patterns](.pair/knowledge/guidelines/architecture/design-patterns/README.md) - [Bounded Context Patterns and Implementation](.pair/knowledge/guidelines/architecture/design-patterns/bounded-contexts.md) - [Context Map Inline-Maintenance Guideline](.pair/knowledge/guidelines/architecture/design-patterns/context-map-maintenance.md) - [Coupling Balance](.pair/knowledge/guidelines/architecture/design-patterns/coupling-balance.md) - [Domain-Driven Design (DDD) Implementation Guide](.pair/knowledge/guidelines/architecture/design-patterns/domain-driven-design.md) - [System Integration Patterns](.pair/knowledge/guidelines/architecture/design-patterns/integration-patterns.md) - [Monorepo Architecture](.pair/knowledge/guidelines/architecture/design-patterns/monorepo.md) +- [Design Patterns](.pair/knowledge/guidelines/architecture/design-patterns/README.md) - [Repository Structure](.pair/knowledge/guidelines/architecture/design-patterns/repository-structure.md) - [Strategic Subdomain Definition Guide](.pair/knowledge/guidelines/architecture/design-patterns/strategic-subdomain-definition.md) - [System Design](.pair/knowledge/guidelines/architecture/design-patterns/system-design.md) - [Workspace Organization](.pair/knowledge/guidelines/architecture/design-patterns/workspace-organization.md) -- [LLM Integration Architecture](.pair/knowledge/guidelines/architecture/llm-integration/README.md) - [Agent Coordination and Communication Patterns](.pair/knowledge/guidelines/architecture/llm-integration/agent-coordination.md) - [AI Workflows and Agent Coordination](.pair/knowledge/guidelines/architecture/llm-integration/ai-workflows.md) - [Model Context Protocol (MCP) Development](.pair/knowledge/guidelines/architecture/llm-integration/mcp-development.md) - [Performance & Security for LLM Integration](.pair/knowledge/guidelines/architecture/llm-integration/performance-security.md) - [RAG Architecture Patterns](.pair/knowledge/guidelines/architecture/llm-integration/rag-architecture.md) +- [LLM Integration Architecture](.pair/knowledge/guidelines/architecture/llm-integration/README.md) - [Vector Databases for LLM Integration](.pair/knowledge/guidelines/architecture/llm-integration/vector-databases.md) -- [Project Architecture Constraints](.pair/knowledge/guidelines/architecture/project-constraints/README.md) - [Implementation Guidelines](.pair/knowledge/guidelines/architecture/project-constraints/deployment-constraints.md) - [Platform & Deployment Constraints](.pair/knowledge/guidelines/architecture/project-constraints/platform-constraints.md) +- [Project Architecture Constraints](.pair/knowledge/guidelines/architecture/project-constraints/README.md) - [Team & Development Constraints](.pair/knowledge/guidelines/architecture/project-constraints/team-constraints.md) -- [Code Design](.pair/knowledge/guidelines/code-design/README.md) -- [Code Organization](.pair/knowledge/guidelines/code-design/code-organization/README.md) +- [Architecture](.pair/knowledge/guidelines/architecture/README.md) - [Feature Architecture](.pair/knowledge/guidelines/code-design/code-organization/feature-architecture.md) - [File Structure](.pair/knowledge/guidelines/code-design/code-organization/file-structure.md) - [Naming Conventions](.pair/knowledge/guidelines/code-design/code-organization/naming-conventions.md) +- [Code Organization](.pair/knowledge/guidelines/code-design/code-organization/README.md) - [Workspace Structure](.pair/knowledge/guidelines/code-design/code-organization/workspace-structure.md) -- [Design Principles](.pair/knowledge/guidelines/code-design/design-principles/README.md) - [Design Rules](.pair/knowledge/guidelines/code-design/design-principles/design-rules.md) - [Error Handling](.pair/knowledge/guidelines/code-design/design-principles/error-handling.md) - [Functional Programming](.pair/knowledge/guidelines/code-design/design-principles/functional-programming.md) - [Mocking Strategy](.pair/knowledge/guidelines/code-design/design-principles/mocking-strategy.md) +- [Design Principles](.pair/knowledge/guidelines/code-design/design-principles/README.md) - [Service Abstraction](.pair/knowledge/guidelines/code-design/design-principles/service-abstraction.md) - [Service Factory](.pair/knowledge/guidelines/code-design/design-principles/service-factory.md) - [SOLID Principles](.pair/knowledge/guidelines/code-design/design-principles/solid-principles.md) -- [Framework Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/README.md) - [React Components](.pair/knowledge/guidelines/code-design/framework-patterns/components.md) - [Dependency Injection Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/dependency-injection.md) - [Fastify Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/fastify.md) - [React Hooks](.pair/knowledge/guidelines/code-design/framework-patterns/hooks.md) - [React & Next.js Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/react-nextjs.md) +- [Framework Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/README.md) - [Repository Pattern](.pair/knowledge/guidelines/code-design/framework-patterns/repository-pattern.md) - [Server Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/server-patterns.md) - [Service Layer](.pair/knowledge/guidelines/code-design/framework-patterns/service-layer.md) - [State Management](.pair/knowledge/guidelines/code-design/framework-patterns/state-management.md) - [TypeScript](.pair/knowledge/guidelines/code-design/framework-patterns/typescript.md) -- [Package Management](.pair/knowledge/guidelines/code-design/package-management/README.md) - [pnpm Package Management](.pair/knowledge/guidelines/code-design/package-management/pnpm.md) +- [Package Management](.pair/knowledge/guidelines/code-design/package-management/README.md) - [Shared Dependencies Management](.pair/knowledge/guidelines/code-design/package-management/shared-dependencies.md) - [Version Catalog Management](.pair/knowledge/guidelines/code-design/package-management/version-catalog.md) - [Workspace Configuration](.pair/knowledge/guidelines/code-design/package-management/workspace-config.md) -- [Quality Standards](.pair/knowledge/guidelines/code-design/quality-standards/README.md) - [Quality Automation](.pair/knowledge/guidelines/code-design/quality-standards/automation.md) - [Code Metrics](.pair/knowledge/guidelines/code-design/quality-standards/code-metrics.md) - [Test Coverage](.pair/knowledge/guidelines/code-design/quality-standards/coverage.md) - [ESLint](.pair/knowledge/guidelines/code-design/quality-standards/eslint.md) - [Linting Tools](.pair/knowledge/guidelines/code-design/quality-standards/linting-tools.md) - [Prettier Formatting](.pair/knowledge/guidelines/code-design/quality-standards/prettier-formatting.md) +- [Quality Standards](.pair/knowledge/guidelines/code-design/quality-standards/README.md) - [Shared Config Packages](.pair/knowledge/guidelines/code-design/quality-standards/shared-config-packages.md) - [Technical Debt Management](.pair/knowledge/guidelines/code-design/quality-standards/technical-debt.md) -- [Collaboration Guidelines](.pair/knowledge/guidelines/collaboration/README.md) -- [Collaboration Automation Framework](.pair/knowledge/guidelines/collaboration/automation/README.md) +- [Code Design](.pair/knowledge/guidelines/code-design/README.md) - [Automation Policy — `tech/automation.md`](.pair/knowledge/guidelines/collaboration/automation/automation-policy.md) - [Azure DevOps Automation](.pair/knowledge/guidelines/collaboration/automation/azure-devops-automation.md) - [Filesystem Automation](.pair/knowledge/guidelines/collaboration/automation/filesystem-automation.md) - [GitHub Automation](.pair/knowledge/guidelines/collaboration/automation/github-automation.md) +- [Collaboration Automation Framework](.pair/knowledge/guidelines/collaboration/automation/README.md) - [Decision Records: ADR, ADL, DDR, and Analysis-Log](.pair/knowledge/guidelines/collaboration/decision-records.md) -- [Estimation Framework](.pair/knowledge/guidelines/collaboration/estimation/README.md) - [AI-Assisted Estimation](.pair/knowledge/guidelines/collaboration/estimation/ai-assisted-estimation.md) - [Complexity-Based Estimation](.pair/knowledge/guidelines/collaboration/estimation/complexity-based-estimation.md) - [Forecast-Based Estimation](.pair/knowledge/guidelines/collaboration/estimation/forecast-based-estimation.md) - [Hybrid Estimation](.pair/knowledge/guidelines/collaboration/estimation/hybrid-estimation.md) +- [Estimation Framework](.pair/knowledge/guidelines/collaboration/estimation/README.md) - [Time-Based Estimation](.pair/knowledge/guidelines/collaboration/estimation/time-based-estimation.md) -- [Issue Management Framework](.pair/knowledge/guidelines/collaboration/issue-management/README.md) - [Azure DevOps Work Items](.pair/knowledge/guidelines/collaboration/issue-management/azure-devops-issues.md) - [Filesystem Issue Tracking](.pair/knowledge/guidelines/collaboration/issue-management/filesystem-issues.md) - [GitHub Issues](.pair/knowledge/guidelines/collaboration/issue-management/github-issues.md) - [Linear Issues](.pair/knowledge/guidelines/collaboration/issue-management/linear-issues.md) -- [Methodology Selection Framework](.pair/knowledge/guidelines/collaboration/methodology/README.md) +- [Issue Management Framework](.pair/knowledge/guidelines/collaboration/issue-management/README.md) - [Kanban Methodology](.pair/knowledge/guidelines/collaboration/methodology/kanban.md) - [Lean Methodology](.pair/knowledge/guidelines/collaboration/methodology/lean.md) - [Large-Scale Scrum (LeSS) Methodology](.pair/knowledge/guidelines/collaboration/methodology/less.md) +- [Methodology Selection Framework](.pair/knowledge/guidelines/collaboration/methodology/README.md) - [SAFe (Scaled Agile Framework)](.pair/knowledge/guidelines/collaboration/methodology/safe.md) - [Scrum Methodology](.pair/knowledge/guidelines/collaboration/methodology/scrum.md) - [Waterfall Methodology](.pair/knowledge/guidelines/collaboration/methodology/waterfall.md) -- [Project Management Tool Framework](.pair/knowledge/guidelines/collaboration/project-management-tool/README.md) - [Azure DevOps - Complete Implementation Guide](.pair/knowledge/guidelines/collaboration/project-management-tool/azure-devops-implementation.md) - [Canonical States & State Mapping](.pair/knowledge/guidelines/collaboration/project-management-tool/canonical-states.md) - [Definition of Ready & Definition of Done](.pair/knowledge/guidelines/collaboration/project-management-tool/definition-of-ready-and-done.md) @@ -292,19 +258,19 @@ - [GitHub Projects - Complete Implementation Guide](.pair/knowledge/guidelines/collaboration/project-management-tool/github-implementation.md) - [Linear - Complete Implementation Guide](.pair/knowledge/guidelines/collaboration/project-management-tool/linear-implementation.md) - [PR State Flow — gate ≠ review](.pair/knowledge/guidelines/collaboration/project-management-tool/pr-states.md) -- [Task-Progress Feedback — checklist ticks + one batched comment](.pair/knowledge/guidelines/collaboration/project-management-tool/task-progress-feedback.md) -- [Project Tracking Framework](.pair/knowledge/guidelines/collaboration/project-tracking/README.md) +- [Project Management Tool Framework](.pair/knowledge/guidelines/collaboration/project-management-tool/README.md) - [Azure DevOps Project Tracking](.pair/knowledge/guidelines/collaboration/project-tracking/azure-devops-tracking.md) - [Filesystem Project Tracking](.pair/knowledge/guidelines/collaboration/project-tracking/filesystem-tracking.md) - [GitHub Project Tracking](.pair/knowledge/guidelines/collaboration/project-tracking/github-tracking.md) -- [Team Collaboration Framework](.pair/knowledge/guidelines/collaboration/team/README.md) +- [Project Tracking Framework](.pair/knowledge/guidelines/collaboration/project-tracking/README.md) +- [Collaboration Guidelines](.pair/knowledge/guidelines/collaboration/README.md) - [Communication Protocols](.pair/knowledge/guidelines/collaboration/team/communication-protocols.md) - [Decision Making](.pair/knowledge/guidelines/collaboration/team/decision-making.md) +- [Team Collaboration Framework](.pair/knowledge/guidelines/collaboration/team/README.md) - [Remote Work](.pair/knowledge/guidelines/collaboration/team/remote-work.md) - [Role Responsibilities](.pair/knowledge/guidelines/collaboration/team/role-responsibilities.md) - [Scenarios](.pair/knowledge/guidelines/collaboration/team/scenarios.md) - [Standards](.pair/knowledge/guidelines/collaboration/team/standards.md) -- [Project Management Templates](.pair/knowledge/guidelines/collaboration/templates/README.md) - [Decision: [Decision Title]](.pair/knowledge/guidelines/collaboration/templates/adl-template.md) - [ADR: [Decision Title]](.pair/knowledge/guidelines/collaboration/templates/adr-template.md) - [Analysis Log: [Analysis Title]](.pair/knowledge/guidelines/collaboration/templates/analysis-log-template.md) @@ -320,88 +286,87 @@ - [Manual Test Case Template](.pair/knowledge/guidelines/collaboration/templates/manual-test-case-template.md) - [Manual Test Report Template](.pair/knowledge/guidelines/collaboration/templates/manual-test-report-template.md) - [Pull Request Template](.pair/knowledge/guidelines/collaboration/templates/pr-template.md) +- [Project Management Templates](.pair/knowledge/guidelines/collaboration/templates/README.md) - [[Subdomain Name] — Context](.pair/knowledge/guidelines/collaboration/templates/subdomain-context-template.md) - [[Subdomain Name] ([Classification] Subdomain)](.pair/knowledge/guidelines/collaboration/templates/subdomain-template.md) - [Task Template](.pair/knowledge/guidelines/collaboration/templates/task-template.md) - [User Story Template](.pair/knowledge/guidelines/collaboration/templates/user-story-template.md) - [Working Area Convention](.pair/knowledge/guidelines/collaboration/working-area.md) -- [🏗️ Infrastructure Knowledge Base](.pair/knowledge/guidelines/infrastructure/README.md) -- [� CI/CD Strategy Practice](.pair/knowledge/guidelines/infrastructure/cicd-strategy/README.md) - [CI/CD Artifacts Management](.pair/knowledge/guidelines/infrastructure/cicd-strategy/artifacts.md) - [GitHub Actions Implementation](.pair/knowledge/guidelines/infrastructure/cicd-strategy/github-actions-implementation.md) +- [� CI/CD Strategy Practice](.pair/knowledge/guidelines/infrastructure/cicd-strategy/README.md) - [Secrets Management](.pair/knowledge/guidelines/infrastructure/cicd-strategy/secrets-management.md) - [CI/CD Strategy](.pair/knowledge/guidelines/infrastructure/cicd-strategy/strategy.md) - [Tier-Aware Pre-Merge Pipeline](.pair/knowledge/guidelines/infrastructure/cicd-strategy/tier-aware-pipeline.md) -- [☁️ Cloud Providers Strategy Practice](.pair/knowledge/guidelines/infrastructure/cloud-providers/README.md) - [AWS Deployment Patterns](.pair/knowledge/guidelines/infrastructure/cloud-providers/aws-deployment.md) - [Cloud Cost Optimization Strategy](.pair/knowledge/guidelines/infrastructure/cloud-providers/cost-optimization.md) - [GCP Deployment Patterns](.pair/knowledge/guidelines/infrastructure/cloud-providers/gcp-deployment.md) - [Multi-Cloud Architecture Strategy](.pair/knowledge/guidelines/infrastructure/cloud-providers/multi-cloud.md) - [Cloud Provider Evaluation Framework](.pair/knowledge/guidelines/infrastructure/cloud-providers/provider-evaluation.md) +- [☁️ Cloud Providers Strategy Practice](.pair/knowledge/guidelines/infrastructure/cloud-providers/README.md) - [Vercel Deployment Patterns](.pair/knowledge/guidelines/infrastructure/cloud-providers/vercel-deployment.md) -- [☁️ Cloud Services Integration Practice](.pair/knowledge/guidelines/infrastructure/cloud-services/README.md) - [Cloud Compute Services](.pair/knowledge/guidelines/infrastructure/cloud-services/cloud-compute.md) - [Cloud Database Services](.pair/knowledge/guidelines/infrastructure/cloud-services/cloud-databases.md) - [Cloud DevOps Services](.pair/knowledge/guidelines/infrastructure/cloud-services/cloud-devops.md) - [Cloud Storage Services](.pair/knowledge/guidelines/infrastructure/cloud-services/cloud-storage.md) -- [🐳 Container Orchestration Practice](.pair/knowledge/guidelines/infrastructure/container-orchestration/README.md) +- [☁️ Cloud Services Integration Practice](.pair/knowledge/guidelines/infrastructure/cloud-services/README.md) - [Container Strategy](.pair/knowledge/guidelines/infrastructure/container-orchestration/container-strategy.md) - [Docker Compose Implementation](.pair/knowledge/guidelines/infrastructure/container-orchestration/docker-compose.md) - [Docker Implementation](.pair/knowledge/guidelines/infrastructure/container-orchestration/docker.md) - [Kubernetes Implementation](.pair/knowledge/guidelines/infrastructure/container-orchestration/kubernetes.md) -- [🚀 Deployment Patterns Practice](.pair/knowledge/guidelines/infrastructure/deployment-patterns/README.md) +- [🐳 Container Orchestration Practice](.pair/knowledge/guidelines/infrastructure/container-orchestration/README.md) - [🚀 Deployment Strategies](.pair/knowledge/guidelines/infrastructure/deployment-patterns/deployment-strategies.md) - [📊 Deployment Monitoring](.pair/knowledge/guidelines/infrastructure/deployment-patterns/monitoring.md) - [⚡ Deployment Performance Optimization](.pair/knowledge/guidelines/infrastructure/deployment-patterns/performance.md) +- [🚀 Deployment Patterns Practice](.pair/knowledge/guidelines/infrastructure/deployment-patterns/README.md) - [🔒 Deployment Security](.pair/knowledge/guidelines/infrastructure/deployment-patterns/security.md) -- [🌍 Environment Management Practice](.pair/knowledge/guidelines/infrastructure/environments/README.md) - [⚙️ Environment Configuration Management](.pair/knowledge/guidelines/infrastructure/environments/environment-config.md) - [🔄 Environment Consistency](.pair/knowledge/guidelines/infrastructure/environments/environment-consistency.md) - [💻 Local Development Environment](.pair/knowledge/guidelines/infrastructure/environments/local-development.md) - [🏭 Production Environment Management](.pair/knowledge/guidelines/infrastructure/environments/production-development.md) +- [🌍 Environment Management Practice](.pair/knowledge/guidelines/infrastructure/environments/README.md) - [🔍 Service Discovery Infrastructure](.pair/knowledge/guidelines/infrastructure/environments/service-discovery.md) - [🎭 Staging Environment Management](.pair/knowledge/guidelines/infrastructure/environments/staging-development.md) -- [🏗️ Infrastructure as Code Practice](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/README.md) - [🤖 Infrastructure Automation](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/automation.md) - [☁️ AWS CDK Implementation Guide](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/aws-cdk-implementation.md) - [📚 Infrastructure as Code Best Practices](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/iac-best-practices.md) - [🎯 Infrastructure Operational Excellence](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/operational-excellence.md) +- [🏗️ Infrastructure as Code Practice](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/README.md) - [🗄️ Infrastructure State Management](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/state-management.md) - [🏗️ Terraform Implementation Guide](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/terraform.md) -- [🧪 Testing Infrastructure Practice](.pair/knowledge/guidelines/infrastructure/testing-infrastructure/README.md) +- [🏗️ Infrastructure Knowledge Base](.pair/knowledge/guidelines/infrastructure/README.md) - [⚡ Performance Testing Infrastructure](.pair/knowledge/guidelines/infrastructure/testing-infrastructure/performance-testing.md) +- [🧪 Testing Infrastructure Practice](.pair/knowledge/guidelines/infrastructure/testing-infrastructure/README.md) - [🗄️ Test Database Management](.pair/knowledge/guidelines/infrastructure/testing-infrastructure/test-databases.md) - [🧪 Test Environment Management](.pair/knowledge/guidelines/infrastructure/testing-infrastructure/test-environments.md) -- [Observability Guidelines](.pair/knowledge/guidelines/observability/README.md) - [AI-Enhanced Observability](.pair/knowledge/guidelines/observability/ai-enhanced-observability.md) -- [Alerting Guidelines](.pair/knowledge/guidelines/observability/alerting/README.md) - [Notification Strategies](.pair/knowledge/guidelines/observability/alerting/notifications.md) +- [Alerting Guidelines](.pair/knowledge/guidelines/observability/alerting/README.md) - [Alerting Strategy](.pair/knowledge/guidelines/observability/alerting/strategy.md) - [Dashboards and Visualization](.pair/knowledge/guidelines/observability/dashboards-visualization.md) - [Distributed Tracing](.pair/knowledge/guidelines/observability/distributed-tracing.md) -- [Metrics Guidelines](.pair/knowledge/guidelines/observability/metrics/README.md) - [Application Monitoring Metrics](.pair/knowledge/guidelines/observability/metrics/application-monitoring.md) - [Business Metrics](.pair/knowledge/guidelines/observability/metrics/business-metrics.md) - [Custom Metrics](.pair/knowledge/guidelines/observability/metrics/custom-metrics.md) - [Feature Usage Metrics](.pair/knowledge/guidelines/observability/metrics/feature-usage.md) - [Performance Metrics](.pair/knowledge/guidelines/observability/metrics/performance-metrics.md) +- [Metrics Guidelines](.pair/knowledge/guidelines/observability/metrics/README.md) - [Metrics Strategy](.pair/knowledge/guidelines/observability/metrics/strategy.md) - [User Experience Metrics](.pair/knowledge/guidelines/observability/metrics/user-experience.md) -- [Observability Principles](.pair/knowledge/guidelines/observability/observability-principles/README.md) - [Proactive Monitoring](.pair/knowledge/guidelines/observability/observability-principles/proactive-monitoring.md) +- [Observability Principles](.pair/knowledge/guidelines/observability/observability-principles/README.md) - [Three Pillars of Observability](.pair/knowledge/guidelines/observability/observability-principles/three-pillars.md) - [Observability Tools](.pair/knowledge/guidelines/observability/observability-tools.md) - [Performance Analysis](.pair/knowledge/guidelines/observability/performance-analysis.md) - [Proactive Detection](.pair/knowledge/guidelines/observability/proactive-detection.md) -- [Structured Logging Guidelines](.pair/knowledge/guidelines/observability/structured-logging/README.md) +- [Observability Guidelines](.pair/knowledge/guidelines/observability/README.md) - [Contextual Information](.pair/knowledge/guidelines/observability/structured-logging/contextual-information.md) - [JSON Logging Standards](.pair/knowledge/guidelines/observability/structured-logging/json-logging.md) - [Log Levels](.pair/knowledge/guidelines/observability/structured-logging/log-levels.md) - [Logging Standards](.pair/knowledge/guidelines/observability/structured-logging/logging-standards.md) +- [Structured Logging Guidelines](.pair/knowledge/guidelines/observability/structured-logging/README.md) - [Sensitive Data Protection](.pair/knowledge/guidelines/observability/structured-logging/sensitive-data-protection.md) - [Workflow Integration](.pair/knowledge/guidelines/observability/workflow-integration.md) -- [Quality Assurance Framework](.pair/knowledge/guidelines/quality-assurance/README.md) -- [Accessibility Framework](.pair/knowledge/guidelines/quality-assurance/accessibility/README.md) - [Assistive Technology Integration](.pair/knowledge/guidelines/quality-assurance/accessibility/assistive-technology.md) - [automated-testing](.pair/knowledge/guidelines/quality-assurance/accessibility/automated-testing.md) - [Browser Extensions for Accessibility Testing](.pair/knowledge/guidelines/quality-assurance/accessibility/browser-extensions.md) @@ -417,6 +382,7 @@ - [Platform-Specific Accessibility](.pair/knowledge/guidelines/quality-assurance/accessibility/platform-specific.md) - [POUR Principles Implementation](.pair/knowledge/guidelines/quality-assurance/accessibility/pour-principles.md) - [React TypeScript Accessibility Patterns](.pair/knowledge/guidelines/quality-assurance/accessibility/react-typescript-patterns.md) +- [Accessibility Framework](.pair/knowledge/guidelines/quality-assurance/accessibility/README.md) - [ShadCN UI Accessibility Integration Guide](.pair/knowledge/guidelines/quality-assurance/accessibility/shadcn-ui-integration.md) - [Accessibility Testing Tools Framework](.pair/knowledge/guidelines/quality-assurance/accessibility/testing-tools.md) - [Accessibility Training Materials Framework](.pair/knowledge/guidelines/quality-assurance/accessibility/training-materials.md) @@ -429,7 +395,6 @@ - [Delivery Metrics](.pair/knowledge/guidelines/quality-assurance/delivery-metrics.md) - [Manual Testing Guidelines](.pair/knowledge/guidelines/quality-assurance/manual-testing.md) - [Manual Verification Framework](.pair/knowledge/guidelines/quality-assurance/manual-verification.md) -- [Performance Optimization Framework](.pair/knowledge/guidelines/quality-assurance/performance/README.md) - [Performance Benchmarking Framework](.pair/knowledge/guidelines/quality-assurance/performance/benchmarking.md) - [Cumulative Layout Shift (CLS) Optimization](.pair/knowledge/guidelines/quality-assurance/performance/cls.md) - [Performance Continuous Improvement Framework](.pair/knowledge/guidelines/quality-assurance/performance/continuous-improvement.md) @@ -446,23 +411,24 @@ - [Performance-First Development Framework](.pair/knowledge/guidelines/quality-assurance/performance/performance-first-development.md) - [⚡ Performance Fundamentals](.pair/knowledge/guidelines/quality-assurance/performance/performance-fundamentals.md) - [Performance Tools and Measurement](.pair/knowledge/guidelines/quality-assurance/performance/performance-tools.md) +- [Performance Optimization Framework](.pair/knowledge/guidelines/quality-assurance/performance/README.md) - [Performance Targets and Benchmarks Framework](.pair/knowledge/guidelines/quality-assurance/performance/targets-benchmarks.md) - [Performance Testing Strategies](.pair/knowledge/guidelines/quality-assurance/performance/testing-strategies.md) - [User-Centric Performance Framework](.pair/knowledge/guidelines/quality-assurance/performance/user-centric-performance.md) - [Quality Model](.pair/knowledge/guidelines/quality-assurance/quality-model.md) -- [Quality Monitoring Framework](.pair/knowledge/guidelines/quality-assurance/quality-monitoring/README.md) - [Code Quality Monitoring](.pair/knowledge/guidelines/quality-assurance/quality-monitoring/code-quality.md) - [Observability Requirements](.pair/knowledge/guidelines/quality-assurance/quality-monitoring/observability-requirements.md) - [Performance Gates Implementation](.pair/knowledge/guidelines/quality-assurance/quality-monitoring/performance-gates.md) -- [Quality Standards Framework](.pair/knowledge/guidelines/quality-assurance/quality-standards/README.md) +- [Quality Monitoring Framework](.pair/knowledge/guidelines/quality-assurance/quality-monitoring/README.md) - [Quality Assurance Checklist](.pair/knowledge/guidelines/quality-assurance/quality-standards/checklist.md) - [Code Review Standards](.pair/knowledge/guidelines/quality-assurance/quality-standards/code-review.md) - [Definition of Done](.pair/knowledge/guidelines/quality-assurance/quality-standards/definition-of-done.md) - [Quality Improvement Process# Quality Improvement Process](.pair/knowledge/guidelines/quality-assurance/quality-standards/improvement-process.md) - [Quality Gates Framework](.pair/knowledge/guidelines/quality-assurance/quality-standards/quality-gates.md) +- [Quality Standards Framework](.pair/knowledge/guidelines/quality-assurance/quality-standards/README.md) - [Quality Responsibility Matrix](.pair/knowledge/guidelines/quality-assurance/quality-standards/responsibility-matrix.md) - [Quality Verification Methods](.pair/knowledge/guidelines/quality-assurance/quality-standards/verification-methods.md) -- [Security Framework](.pair/knowledge/guidelines/quality-assurance/security/README.md) +- [Quality Assurance Framework](.pair/knowledge/guidelines/quality-assurance/README.md) - [AI-Enhanced Security Framework](.pair/knowledge/guidelines/quality-assurance/security/ai-enhanced-security.md) - [API Security Implementation](.pair/knowledge/guidelines/quality-assurance/security/api-security.md) - [🔐 Authentication and Authorization](.pair/knowledge/guidelines/quality-assurance/security/authentication-authorization.md) @@ -473,6 +439,7 @@ - [Dependency Security Management](.pair/knowledge/guidelines/quality-assurance/security/dependency-security.md) - [Dependency Security Testing Framework](.pair/knowledge/guidelines/quality-assurance/security/dependency-testing.md) - [Incident Response Framework](.pair/knowledge/guidelines/quality-assurance/security/incident-response.md) +- [Security Framework](.pair/knowledge/guidelines/quality-assurance/security/README.md) - [Risk-Based Security Framework](.pair/knowledge/guidelines/quality-assurance/security/risk-based-security.md) - [SAST Static Testing](.pair/knowledge/guidelines/quality-assurance/security/sast-static-testing.md) - [Secret Scanning — Deterministic CI Layer](.pair/knowledge/guidelines/quality-assurance/security/secret-scanning.md) @@ -489,17 +456,15 @@ - [Vulnerability Assessment](.pair/knowledge/guidelines/quality-assurance/security/vulnerability-assessment.md) - [Vulnerability Prevention Framework](.pair/knowledge/guidelines/quality-assurance/security/vulnerability-prevention.md) - [Web Application Security Framework](.pair/knowledge/guidelines/quality-assurance/security/web-app-security.md) -- [Technical Standards](.pair/knowledge/guidelines/technical-standards/README.md) -- [AI Development Standards](.pair/knowledge/guidelines/technical-standards/ai-development/README.md) -- [Agent Harness Framework](.pair/knowledge/guidelines/technical-standards/ai-development/agent-harness/README.md) +- [📚 Technical Guidelines Knowledge Base](.pair/knowledge/guidelines/README.md) - [Claude Code](.pair/knowledge/guidelines/technical-standards/ai-development/agent-harness/claude-code.md) - [opencode](.pair/knowledge/guidelines/technical-standards/ai-development/agent-harness/opencode.md) - [pi](.pair/knowledge/guidelines/technical-standards/ai-development/agent-harness/pi.md) +- [Agent Harness Framework](.pair/knowledge/guidelines/technical-standards/ai-development/agent-harness/README.md) - [AI Development Tools](.pair/knowledge/guidelines/technical-standards/ai-development/ai-tools.md) - [AI Development Documentation Standards](.pair/knowledge/guidelines/technical-standards/ai-development/documentation-standards.md) - [Model Context Protocol (MCP) Integration](.pair/knowledge/guidelines/technical-standards/ai-development/mcp-integration.md) -- [Process Profiles](.pair/knowledge/guidelines/technical-standards/ai-development/process-profiles.md) -- [Skill Conventions — Shared KB References](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/README.md) +- [AI Development Standards](.pair/knowledge/guidelines/technical-standards/ai-development/README.md) - [Adoption-Informed Generation (decision log + ADR + context map)](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/adoption-informed-generation.md) - [Approval Rounds and the `$approval` Signal](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/approval-rounds.md) - [Graceful Degradation — Standard Bullets](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/graceful-degradation.md) @@ -507,105 +472,105 @@ - [Idempotency Convention](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/idempotency.md) - [Nested Sub-Documents (Progressive Disclosure)](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/nested-sub-documents.md) - [Output Format Shapes](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/output-shapes.md) -- [Process-Profile Gate — Direct Invocation of a Disabled Step](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/process-profile-gate.md) +- [Skill Conventions — Shared KB References](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/README.md) - [`/pair-capability-record-decision` Invocation Contract](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/record-decision-contract.md) - [Resolution Cascade](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/resolution-cascade.md) - [Story-Local Acceptance-Criterion Markers — Banned](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/story-local-markers.md) - [Template Resolution](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/template-resolution.md) - [To-Issues Triage (Extend vs Create)](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/to-issues-triage.md) - [Way-of-Working / PM-Tool + Code-Host Resolution](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/way-of-working-pm-resolution.md) -- [Process Step Catalogue](.pair/knowledge/guidelines/technical-standards/ai-development/step-catalogue.md) -- [Coding Standards](.pair/knowledge/guidelines/technical-standards/coding-standards/README.md) - [Error Handling Standards](.pair/knowledge/guidelines/technical-standards/coding-standards/error-handling.md) - [Internationalization and Localization (i18n/l10n)](.pair/knowledge/guidelines/technical-standards/coding-standards/i18n-localization.md) +- [Coding Standards](.pair/knowledge/guidelines/technical-standards/coding-standards/README.md) - [Technical Debt Management](.pair/knowledge/guidelines/technical-standards/coding-standards/technical-debt.md) - [Versioning Standards](.pair/knowledge/guidelines/technical-standards/coding-standards/versioning.md) -- [Deployment Workflow](.pair/knowledge/guidelines/technical-standards/deployment-workflow/README.md) - [Build Standards](.pair/knowledge/guidelines/technical-standards/deployment-workflow/build-standards.md) - [Deployment Automation](.pair/knowledge/guidelines/technical-standards/deployment-workflow/deployment-automation.md) +- [Deployment Workflow](.pair/knowledge/guidelines/technical-standards/deployment-workflow/README.md) - [Release Management](.pair/knowledge/guidelines/technical-standards/deployment-workflow/release-management.md) - [Deployment Strategy](.pair/knowledge/guidelines/technical-standards/deployment-workflow/strategy.md) -- [Development Tools Standards](.pair/knowledge/guidelines/technical-standards/development-tools/README.md) - [Development Environment Setup](.pair/knowledge/guidelines/technical-standards/development-tools/environment-setup.md) +- [Development Tools Standards](.pair/knowledge/guidelines/technical-standards/development-tools/README.md) - [Recommended Tools](.pair/knowledge/guidelines/technical-standards/development-tools/recommended-tools.md) - [Required Tools](.pair/knowledge/guidelines/technical-standards/development-tools/required-tools.md) - [Tool Configuration](.pair/knowledge/guidelines/technical-standards/development-tools/tool-configuration.md) - [Workflow Tools](.pair/knowledge/guidelines/technical-standards/development-tools/workflow-tools.md) - [Feature Flags](.pair/knowledge/guidelines/technical-standards/feature-flags.md) -- [Git Workflow Standards](.pair/knowledge/guidelines/technical-standards/git-workflow/README.md) - [Git Development Process](.pair/knowledge/guidelines/technical-standards/git-workflow/development-process.md) - [Git Quality Assurance Process](.pair/knowledge/guidelines/technical-standards/git-workflow/quality-assurance.md) +- [Git Workflow Standards](.pair/knowledge/guidelines/technical-standards/git-workflow/README.md) - [Version Control Standards](.pair/knowledge/guidelines/technical-standards/git-workflow/version-control.md) -- [Integration Standards](.pair/knowledge/guidelines/technical-standards/integration-standards/README.md) - [API Design Standards](.pair/knowledge/guidelines/technical-standards/integration-standards/api-design.md) - [Data Management Standards](.pair/knowledge/guidelines/technical-standards/integration-standards/data-management.md) - [External Services Integration](.pair/knowledge/guidelines/technical-standards/integration-standards/external-services.md) - [Integration Patterns](.pair/knowledge/guidelines/technical-standards/integration-standards/integration-patterns.md) -- [Technology Stack Standards](.pair/knowledge/guidelines/technical-standards/technology-stack/README.md) +- [Integration Standards](.pair/knowledge/guidelines/technical-standards/integration-standards/README.md) +- [Technical Standards](.pair/knowledge/guidelines/technical-standards/README.md) - [Technology Stack Conventions](.pair/knowledge/guidelines/technical-standards/technology-stack/conventions.md) - [Framework Selection Guidelines](.pair/knowledge/guidelines/technical-standards/technology-stack/framework-selection.md) +- [Technology Stack Standards](.pair/knowledge/guidelines/technical-standards/technology-stack/README.md) - [Technology Stack Standards](.pair/knowledge/guidelines/technical-standards/technology-stack/stack-standards.md) - [Technical Decisions Framework](.pair/knowledge/guidelines/technical-standards/technology-stack/tech-decisions.md) -- [🧪 Testing](.pair/knowledge/guidelines/testing/README.md) -- [♿ Accessibility Testing](.pair/knowledge/guidelines/testing/accessibility-testing/README.md) - [Automated Accessibility Testing](.pair/knowledge/guidelines/testing/accessibility-testing/automated-a11y.md) - [Manual Accessibility Testing](.pair/knowledge/guidelines/testing/accessibility-testing/manual-a11y.md) -- [🎭 End-to-End Testing](.pair/knowledge/guidelines/testing/e2e-testing/README.md) +- [♿ Accessibility Testing](.pair/knowledge/guidelines/testing/accessibility-testing/README.md) - [Cypress Testing](.pair/knowledge/guidelines/testing/e2e-testing/cypress.md) - [Playwright Testing](.pair/knowledge/guidelines/testing/e2e-testing/playwright.md) +- [🎭 End-to-End Testing](.pair/knowledge/guidelines/testing/e2e-testing/README.md) - [Test Scenarios](.pair/knowledge/guidelines/testing/e2e-testing/test-scenarios.md) -- [🔗 Integration Testing](.pair/knowledge/guidelines/testing/integration-testing/README.md) - [API Testing Strategy and Implementation](.pair/knowledge/guidelines/testing/integration-testing/api-testing.md) - [Database Testing Strategy and Implementation](.pair/knowledge/guidelines/testing/integration-testing/database-testing.md) +- [🔗 Integration Testing](.pair/knowledge/guidelines/testing/integration-testing/README.md) - [Service Integration](.pair/knowledge/guidelines/testing/integration-testing/service-integration.md) -- [⚡ Performance Testing](.pair/knowledge/guidelines/testing/performance-testing/README.md) - [Benchmarking](.pair/knowledge/guidelines/testing/performance-testing/benchmarking.md) - [Load Testing](.pair/knowledge/guidelines/testing/performance-testing/load-testing.md) +- [⚡ Performance Testing](.pair/knowledge/guidelines/testing/performance-testing/README.md) - [Stress Testing](.pair/knowledge/guidelines/testing/performance-testing/stress-testing.md) -- [🤖 Test Automation](.pair/knowledge/guidelines/testing/test-automation/README.md) +- [🧪 Testing](.pair/knowledge/guidelines/testing/README.md) - [CI Integration](.pair/knowledge/guidelines/testing/test-automation/ci-integration.md) +- [🤖 Test Automation](.pair/knowledge/guidelines/testing/test-automation/README.md) - [Test Reporting](.pair/knowledge/guidelines/testing/test-automation/test-reporting.md) -- [🎯 Testing Strategy](.pair/knowledge/guidelines/testing/test-strategy/README.md) - [Behavior Driven Development (BDD)](.pair/knowledge/guidelines/testing/test-strategy/bdd-behavior-driven-development.md) - [Coverage Strategy](.pair/knowledge/guidelines/testing/test-strategy/coverage-strategy.md) +- [🎯 Testing Strategy](.pair/knowledge/guidelines/testing/test-strategy/README.md) - [Test Driven Development (TDD)](.pair/knowledge/guidelines/testing/test-strategy/tdd-test-driven-development.md) - [Test Pyramid](.pair/knowledge/guidelines/testing/test-strategy/test-pyramid.md) - [Testing Philosophy](.pair/knowledge/guidelines/testing/test-strategy/testing-philosophy.md) -- [⚡ Unit Testing](.pair/knowledge/guidelines/testing/unit-testing/README.md) - [Jest Configuration](.pair/knowledge/guidelines/testing/unit-testing/jest-configuration.md) - [Mocking Strategies](.pair/knowledge/guidelines/testing/unit-testing/mocking-strategies.md) +- [⚡ Unit Testing](.pair/knowledge/guidelines/testing/unit-testing/README.md) - [Unit Testing Patterns](.pair/knowledge/guidelines/testing/unit-testing/test-patterns.md) - [Vitest Setup](.pair/knowledge/guidelines/testing/unit-testing/vitest-setup.md) -- [🎨 User Experience Guidelines](.pair/knowledge/guidelines/user-experience/README.md) - [Asset Collection](.pair/knowledge/guidelines/user-experience/asset-collection.md) - [Brand Alignment](.pair/knowledge/guidelines/user-experience/brand-alignment.md) - [CAT Tools (Computer-Assisted Translation)](.pair/knowledge/guidelines/user-experience/cat-tools.md) -- [Content Strategy](.pair/knowledge/guidelines/user-experience/content-strategy/README.md) - [Communication Design](.pair/knowledge/guidelines/user-experience/content-strategy/communication-design.md) - [Content Guidelines](.pair/knowledge/guidelines/user-experience/content-strategy/content-guidelines.md) - [Information Architecture](.pair/knowledge/guidelines/user-experience/content-strategy/information-architecture.md) +- [Content Strategy](.pair/knowledge/guidelines/user-experience/content-strategy/README.md) - [Translation Management](.pair/knowledge/guidelines/user-experience/content-strategy/translation-management.md) -- [Design Principles](.pair/knowledge/guidelines/user-experience/design-principles/README.md) - [Accessibility Integration](.pair/knowledge/guidelines/user-experience/design-principles/accessibility-integration.md) - [Color Contrast](.pair/knowledge/guidelines/user-experience/design-principles/color-contrast.md) - [🎯 Consistency Standards](.pair/knowledge/guidelines/user-experience/design-principles/consistency-standards.md) - [Layout Spacing](.pair/knowledge/guidelines/user-experience/design-principles/layout-spacing.md) +- [Design Principles](.pair/knowledge/guidelines/user-experience/design-principles/README.md) - [Typography](.pair/knowledge/guidelines/user-experience/design-principles/typography.md) - [👥 User-Centered Design](.pair/knowledge/guidelines/user-experience/design-principles/user-centered-design.md) -- [Design Systems](.pair/knowledge/guidelines/user-experience/design-systems/README.md) - [🧩 Component Libraries](.pair/knowledge/guidelines/user-experience/design-systems/component-libraries.md) - [🎨 Design Tokens](.pair/knowledge/guidelines/user-experience/design-systems/design-tokens.md) +- [Design Systems](.pair/knowledge/guidelines/user-experience/design-systems/README.md) - [System Architecture](.pair/knowledge/guidelines/user-experience/design-systems/system-architecture.md) - [Tailwind ShadCN Integration](.pair/knowledge/guidelines/user-experience/design-systems/tailwind-shadcn.md) - [Figma Workflows](.pair/knowledge/guidelines/user-experience/figma-workflows.md) -- [Interface Design](.pair/knowledge/guidelines/user-experience/interface-design/README.md) - [Component Design](.pair/knowledge/guidelines/user-experience/interface-design/component-design.md) - [Interaction Design](.pair/knowledge/guidelines/user-experience/interface-design/interaction-design.md) - [Layout Principles](.pair/knowledge/guidelines/user-experience/interface-design/layout-principles.md) +- [Interface Design](.pair/knowledge/guidelines/user-experience/interface-design/README.md) - [Responsive Principles](.pair/knowledge/guidelines/user-experience/interface-design/responsive-principles.md) - [UI Patterns](.pair/knowledge/guidelines/user-experience/interface-design/ui-patterns.md) - [Visual Standards](.pair/knowledge/guidelines/user-experience/interface-design/visual-standards.md) - [Markdown Templates](.pair/knowledge/guidelines/user-experience/markdown-templates.md) +- [🎨 User Experience Guidelines](.pair/knowledge/guidelines/user-experience/README.md) - [User Research](.pair/knowledge/guidelines/user-experience/user-research/README.md) - [🔬 Research Methods](.pair/knowledge/guidelines/user-experience/user-research/research-methods.md) - [Testing and Validation](.pair/knowledge/guidelines/user-experience/user-research/testing-validation.md) diff --git a/apps/pair-cli/src/cli.e2e.test.ts b/apps/pair-cli/src/cli.e2e.test.ts index c2c0ee2e5..359d3d81b 100644 --- a/apps/pair-cli/src/cli.e2e.test.ts +++ b/apps/pair-cli/src/cli.e2e.test.ts @@ -1,5 +1,9 @@ -import { describe, it, expect, vi } from 'vitest' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' import { InMemoryFileSystemService } from '@pair/content-ops/test-utils/in-memory-fs' +import { fileSystemService } from '@pair/content-ops' import { installCommand, handleInstallCommand, @@ -8,7 +12,9 @@ import { handlePackageCommand, handleScaffoldKbCommand, handleKbInfoCommand, + commandRegistry, } from './commands' +import type { IterationResult } from './commands/run/stream-reader' /** * pair-cli e2e suite. @@ -57,7 +63,7 @@ describe('pair-cli e2e', () => { const fs = new InMemoryFileSystemService(seed, projectRoot, projectRoot) // 2. Perform installation to disjoint target - // pair-cli install /opt/pair/kb --source /mnt/external/kb-dataset + // pair install /opt/pair/kb --source /mnt/external/kb-dataset await installCommand(fs, ['--source', kbSourceDir], { baseTarget: disjointTarget, useDefaults: true, @@ -73,7 +79,7 @@ describe('pair-cli e2e', () => { // Add new file to source await fs.writeFile(`${kbSourceDir}/knowledge/new.md`, 'New content') - // pair-cli update /opt/pair/kb --source /mnt/external/kb-dataset + // pair update /opt/pair/kb --source /mnt/external/kb-dataset await handleUpdateCommand( { command: 'update', @@ -89,7 +95,7 @@ describe('pair-cli e2e', () => { expect(fs.existsSync(`${disjointTarget}/knowledge/new.md`)).toBe(true) // 5. Test disjoint update-link - // pair-cli update-link /opt/pair/kb + // pair update-link /opt/pair/kb await handleUpdateLinkCommand( { command: 'update-link', @@ -111,7 +117,7 @@ describe('pair-cli e2e', () => { * (scaffold output is the package input; the package/scaffold registry declaration * is what install consumes), proving the scaffold needs no install special-casing. */ - it('scaffolds an external KB repo, packages it with pair-cli package, and installs it into a separate project', async () => { + it('scaffolds an external KB repo, packages it with pair package, and installs it into a separate project', async () => { const moduleDir = '/opt/pair-cli' const kbRepo = '/work/acme-kb' const consumer = '/work/consumer' @@ -207,7 +213,7 @@ describe('pair-cli e2e', () => { projectRoot, ) - /** `pair-cli kb-info --json` (version-check mode), returning exit code + parsed report. */ + /** `pair kb-info --json` (version-check mode), returning exit code + parsed report. */ async function versionCheck() { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) try { @@ -259,4 +265,264 @@ describe('pair-cli e2e', () => { expect(clean.report.migrationUrl).toBeUndefined() }) }) + + /** + * US-217 T-5 — tag-driven dispatch against a POPULATED BOARD, end to end. + * + * Genuinely e2e by this suite's own bar (see the header): the run is driven through the command + * registry the CLI dispatches on, against a REAL project directory, and each dispatch hands state + * to the next one through artifacts on disk — the per-card lock and the appended audit file. The + * module suites prove each decision in isolation with the lock and the audit writer injected; + * what only this level can show is that five triggers fired at one project leave exactly the runs, + * the trail and the locks they should, with nothing shared between them but the filesystem. + * + * The board is the fixture: one card per case the story names — routed, untagged, eligible but + * unmapped, multi-tagged, mapped but ineligible. The ONLY injected dependency is the engine spawn, + * because a test that starts a real agent is not a test. + */ + describe('tag-driven dispatch on a populated board (US-217)', () => { + const POLICY = `## Eligibility + +risk:green + +## Workflows + +auto-plan ⇒ pair-process-plan-tasks +auto-dev ⇒ pair-loop +Precedence: auto-plan, auto-dev +` + + /** + * The cards a trigger fires on, with the labels it observed at that moment — and everything + * each one must produce. + * + * The fixture IS the assertion set: `routes` is the prompt the engine must be given (absent ⇒ + * nothing may spawn, nothing may be recorded on the card) and `trail` is what the audit file + * must say about that card. Every check below iterates these rows, so a row added here is a + * row checked, and a row whose workflow, scoping argument or skip reason changes fails on its + * own row instead of shifting a positional index under an assertion about a different card. + */ + const BOARD = [ + // pair-loop declares `--iteration` too, and plan-tasks does not: each invocation carries + // exactly the arguments its own `## Arguments` table declares, and nothing else. + { + card: '301', + tags: ['auto-dev', 'risk:green'], + routes: '/pair-loop --root 301 --iteration 1', + trail: [ + /event=start card=301 tag=auto-dev workflow=pair-loop/, + /event=end card=301 .*outcome=completed/, + ], + }, + // 302 is the UNLABELLED card — the state a host adapter renders as an empty `--card-tags`. + // It stops at the ELIGIBILITY gate, before its (absent) tags are ever routed: an untagged + // card matches no eligibility label either, so the earliest guard is the one that catches it. + { + card: '302', + tags: [], + routes: undefined, + trail: [/event=skip card=302 reason=ineligible/], + }, + // 303 IS eligible and still runs nothing: eligibility selects, the mapping routes, and there + // is no default workflow for a card the mapping does not name. + { + card: '303', + tags: ['risk:green'], + routes: undefined, + trail: [/event=skip card=303 reason=unmapped/], + }, + { + card: '304', + tags: ['auto-plan', 'auto-dev', 'risk:green'], + // The DECLARED precedence wins over the first mapped tag the card carries — and the card + // reaches plan-tasks as `--story`, the argument that skill declares: `--root 304` is a + // scope it never sees, and its Step 0 would then pick the top story on the board. + routes: '/pair-process-plan-tasks --story 304', + trail: [/event=start card=304 tag=auto-plan/], + }, + { + card: '305', + tags: ['auto-dev'], + routes: undefined, + trail: [/event=skip card=305 reason=ineligible/], + }, + ] as const + + type BoardRow = (typeof BOARD)[number] + /** The cards the board expects to run, in trigger order — the fixture, read as data. */ + const routed = BOARD.filter( + (row): row is BoardRow & { routes: string } => row.routes !== undefined, + ) + const unrouted = BOARD.filter(row => row.routes === undefined) + + const AUDIT = '.pair/working/automation/loop-audit.md' + const LOCKS = '.pair/working/automation/locks' + + let project: string + let spawned: string[] + let printed: string[] + let log: ReturnType + + const write = (relative: string, content: string): void => { + const target = join(project, relative) + mkdirSync(join(target, '..'), { recursive: true }) + writeFileSync(target, content) + } + + beforeEach(() => { + project = mkdtempSync(join(tmpdir(), 'pair-dispatch-e2e-')) + spawned = [] + printed = [] + + write( + 'config.json', + JSON.stringify({ + asset_registries: { + skills: { + source: '.skills', + behavior: 'overwrite', + description: 'skills', + prefix: 'pair', + targets: [{ path: '.claude/skills/', mode: 'canonical' }], + }, + }, + }), + ) + // The installed skill set the mapping is resolved against — both declared workflows. + write('.claude/skills/pair-loop/SKILL.md', '') + write('.claude/skills/pair-process-plan-tasks/SKILL.md', '') + write('.pair/adoption/tech/automation.md', POLICY) + // A `claude` on PATH: engine resolution probes the filesystem, and the default cascade + // resolves the schema default when nothing declares one. + write('bin/claude', '') + vi.stubEnv('PATH', join(project, 'bin')) + + log = vi.spyOn(console, 'log').mockImplementation(line => { + printed.push(String(line)) + }) + }) + + afterEach(() => { + log.mockRestore() + vi.unstubAllEnvs() + rmSync(project, { recursive: true, force: true }) + }) + + /** One trigger event, through the registry the CLI dispatches on. */ + const trigger = async ( + card: string, + tags: readonly string[], + runIteration?: () => Promise, + ): Promise => + commandRegistry.run.handle( + commandRegistry.run.parse({ + card, + cardTags: tags.join(','), + cwd: project, + maxIterations: 1, + }), + fileSystemService, + { + runIteration: async input => { + spawned.push(input.promptText) + return runIteration ? await runIteration() : { outcome: 'success', detail: 'done' } + }, + }, + ) + + const auditTrail = (): string => readFileSync(join(project, AUDIT), 'utf-8') + + it('runs exactly the two cards the mapping routes, and leaves the trail to prove the other three', async () => { + for (const { card, tags } of BOARD) expect(await trigger(card, tags)).toBe(0) + + // AC1 — routed cards ran the MAPPED workflow, scoped to their own card, and NOTHING else ran. + // Driven off the fixture: every row that declares a route is checked against the prompt the + // engine was actually given, in the order the triggers fired. + expect(spawned).toEqual(routed.map(row => row.routes)) + + // AC2 — every card left the trail its own row declares, and the ones that ran nothing say + // WHY. Read off the fixture, so a row added above is a row this checks. + const trail = auditTrail() + for (const row of BOARD) for (const line of row.trail) expect(trail).toMatch(line) + // No card was ever routed to a workflow its tags do not name, and none of them started. + for (const { card } of unrouted) { + expect(trail).not.toMatch(new RegExp(`card=${card} (tag|workflow)=`)) + expect(trail).not.toMatch(new RegExp(`event=start card=${card}`)) + } + + // AC3 — the line the host adapter posts on the card exists for the runs that started, and + // ONLY for those: a card that never ran must not get a comment claiming it did. + const records = printed.filter(line => line.startsWith('DISPATCH-RECORD:')) + expect(records).toEqual(routed.map(row => expect.stringContaining(`card=${row.card}`))) + + // Every lock was released: the board is left dispatchable, not parked. + for (const { card } of BOARD) expect(existsSync(join(project, LOCKS, card))).toBe(false) + }) + + /** + * The dispatched card is the ONLY subject a routed run can have — `--root` cannot displace it. + * + * Before the refusal, `--card 301 --root 300` parsed and drove `/pair-loop --root 300` while the + * audit file recorded `card=301` start AND end, the `DISPATCH-RECORD:` line named 301, and the + * exclusive lock was taken on 301 — so the agent worked an unguarded subtree (a second trigger + * on 300 would have acquired its own free lock and started a second agent on the same branch) + * and the trail credited a card nothing ran on. Checked through the registry, because the + * refusal has to hold at the entry point a trigger actually calls. + */ + it('refuses --root on a dispatched card, and spawns nothing when it does', async () => { + expect(() => + commandRegistry.run.parse({ + card: '301', + cardTags: 'auto-dev,risk:green', + root: '300', + cwd: project, + maxIterations: 1, + }), + ).toThrow(/--card cannot be combined with --root/) + + expect(spawned).toHaveLength(0) + expect(existsSync(join(project, AUDIT))).toBe(false) + expect(existsSync(join(project, LOCKS, '301'))).toBe(false) + }) + + it('never starts a second run on a card a run already holds (trigger burst)', async () => { + // The burst, exactly as a host produces it: the second trigger arrives WHILE the first run is + // in flight. Re-entering from inside the iteration is what makes the lock the thing under + // test rather than a sequence of two finished runs. + let reentrant: number | undefined + await trigger('301', ['auto-dev', 'risk:green'], async () => { + reentrant = await trigger('301', ['auto-dev', 'risk:green']) + return { outcome: 'success', detail: 'done' } + }) + + expect(reentrant).toBe(0) + // One spawn, not two: the second dispatch was skipped, never queued behind the first. + expect(spawned).toHaveLength(1) + expect(auditTrail()).toMatch(/event=skip card=301 reason=run-in-progress/) + // ...and the burst did not leave the card locked for the next trigger. + expect(existsSync(join(project, LOCKS, '301'))).toBe(false) + // The skip names the REAL holder — the directory the run probed, and how long it has held it. + // Nothing reaps a lock, so a killed run leaves one behind and every later trigger on the card + // skips forever; the age is what tells an operator this skip is not a healthy burst. + const skip = printed.find(line => line.includes('run-in-progress')) + expect(skip).toContain(join(project, LOCKS, '301')) + expect(skip).toContain('held under a minute') + expect(skip).toMatch(/stale/) + }) + + it('routes nothing at all when the project declares no mapping — the shipped default', async () => { + write('.pair/adoption/tech/automation.md', '## Eligibility\n\nrisk:green\n') + + for (const { card, tags } of BOARD) expect(await trigger(card, tags)).toBe(0) + + expect(spawned).toHaveLength(0) + expect(printed.some(line => line.includes('no mapping declared'))).toBe(true) + // EVERY card on the board, not just the one that would otherwise have routed: with no + // `## Workflows` section nothing is routable, and each card says so in the trail. + const trail = auditTrail() + for (const { card } of BOARD) { + expect(trail).toMatch(new RegExp(`event=skip card=${card} reason=no-mapping-declared`)) + } + }) + }) }) diff --git a/apps/pair-cli/src/commands/run/automation-policy.ts b/apps/pair-cli/src/commands/run/automation-policy.ts index b28ad35cf..8da2b9da6 100644 --- a/apps/pair-cli/src/commands/run/automation-policy.ts +++ b/apps/pair-cli/src/commands/run/automation-policy.ts @@ -1,6 +1,8 @@ import { join } from 'path' import type { FileSystemService } from '@pair/content-ops' import { isLabelShape, isSafePromptText, promptSafetyFailure } from './prompt-safety' +import { assertLabelValue, policyHalt, POLICY_PATH, sectionLines } from './policy-sections' +import { readWorkflowMapping, type WorkflowMapping } from './workflow-mapping' /** * The automation-policy reader (US-451 T-8) — READ-ONLY, and it BORROWS every parameter. @@ -18,7 +20,7 @@ import { isLabelShape, isSafePromptText, promptSafetyFailure } from './prompt-sa * not been read. */ -export const POLICY_PATH = '.pair/adoption/tech/automation.md' +export { POLICY_PATH } from './policy-sections' export const DEFAULT_AUDIT_LOCATION = 'automation/loop-audit.md' /** Absent stop-predicate section ⇒ exactly one iteration, never an unbounded run. */ export const FAIL_SAFE_MAX_ITERATIONS = 1 @@ -44,15 +46,18 @@ export interface AutomationPolicy { readonly maxIterations: number readonly maxParallelism: number readonly auditLocation: string + /** + * `## Workflows`'s tag→workflow mapping (US-217), absent when the project declares none. + * + * Absent is the SHIPPED state and never an error: with no mapping there is no workflow to route a + * card to, so a tag-driven dispatch reports "no mapping declared" and exits cleanly. Automation is + * opt-in per card, and this is the declaration that opts in. + */ + readonly workflows?: WorkflowMapping readonly source: typeof POLICY_PATH | 'fail-safe defaults (policy file absent)' readonly warnings: readonly string[] } -/** A HALT on the policy read: the message names the file and the offending value. */ -function halt(detail: string): never { - throw new Error(`${POLICY_PATH} — ${detail}. Fix the adoption file, then re-run.`) -} - /** * The one message every unsafe value gets, wherever it was declared — the shared rule set lives in * `prompt-safety.ts` so the CLI flags and the policy fields cannot drift apart (round 6, Major). @@ -68,7 +73,7 @@ function halt(detail: string): never { */ function assertSafePromptText(section: string, value: string): void { if (isSafePromptText(value)) return - halt(promptSafetyFailure(`\`## ${section}\``, value)) + policyHalt(promptSafetyFailure(`\`## ${section}\``, value)) } export function readAutomationPolicy(fs: FileSystemService, projectRoot: string): AutomationPolicy { @@ -91,6 +96,7 @@ export function readAutomationPolicy(fs: FileSystemService, projectRoot: string) const warnings: string[] = [] const eligibility = readEligibility(markdown, warnings) const stop = readStopPredicate(markdown) + const workflows = readWorkflowMapping(markdown) return { ...(eligibility !== undefined && { eligibility }), @@ -99,6 +105,7 @@ export function readAutomationPolicy(fs: FileSystemService, projectRoot: string) maxIterations: stop.maxIterations, maxParallelism: readMaxParallelism(markdown), auditLocation: readAuditLocation(markdown), + ...(workflows !== undefined && { workflows }), source: POLICY_PATH, warnings, } @@ -115,59 +122,14 @@ export function describeParallelism(policy: AutomationPolicy): string { return `Parallelism: 1 (policy: ${policy.maxParallelism})` } return ( - `Parallelism: policy declares max ${policy.maxParallelism}, but a single \`pair-cli run\` process ` + + `Parallelism: policy declares max ${policy.maxParallelism}, but a single \`pair run\` process ` + `drives 1 card at a time — run multiple driver processes for concurrency (the batch decision ` + `remains pair-loop's)` ) } -/* ------------------------------------------------------------------ sections */ - -/** - * The body of a level-2 section, as RENDERED markdown: an occurrence inside a fenced code block - * is not a heading (the schema documents its own declarations inside fences, so a line scan that - * ignored fences would read a documentation example as a declaration). - */ -function sectionBodies(markdown: string, heading: string): string[][] { - const bodies: string[][] = [] - let current: string[] | undefined - let fenced = false - - for (const raw of markdown.split(/\r?\n/)) { - const line = raw.trim() - if (line.startsWith('```')) { - fenced = !fenced - if (current) current.push(raw) - continue - } - if (!fenced && /^##\s+/.test(line)) { - if (current) bodies.push(current) - current = line.replace(/^##\s+/, '') === heading ? [] : undefined - continue - } - if (current) current.push(raw) - } - if (current) bodies.push(current) - return bodies -} - -/** The section's non-empty lines, trimmed — the unit every schema rule is stated over. */ -function sectionLines(markdown: string, heading: string): string[] | undefined { - const bodies = sectionBodies(markdown, heading) - if (bodies.length === 0) return undefined - if (bodies.length > 1) { - halt(`carries ${bodies.length} \`## ${heading}\` headings, but exactly one declaration is read`) - } - return bodies[0]!.map(line => line.trim()).filter(line => line.length > 0) -} - /* -------------------------------------------------------------- eligibility */ -// The schema's list, plus a leading SINGLE backtick: an inline-code paste is the same copied-wrapper -// mistake as a fence, and tier 1 already rejected it (round 7, minor 1). -const MARKDOWN_BLOCK_MARKERS = ['`', '-', '*', '+', '>', '#'] -const GITHUB_LABEL_CAP = 50 - /** * `## Eligibility` — exactly one label, validated by the guideline's seven HALT triggers and * then passed to the skill VERBATIM. Validating is not transforming. @@ -180,33 +142,18 @@ function readEligibility(markdown: string, warnings: string[]): string | undefin ) return undefined } - if (lines.length === 0) halt('`## Eligibility` is present but empty (a half-written declaration)') + if (lines.length === 0) + policyHalt('`## Eligibility` is present but empty (a half-written declaration)') if (lines.length > 1) { - halt(`\`## Eligibility\` carries ${lines.length} non-empty lines, but takes exactly one label`) + policyHalt( + `\`## Eligibility\` carries ${lines.length} non-empty lines, but takes exactly one label`, + ) } const value = lines[0]! - // A STANDALONE token, as the schema says and tier 1 matches — `\b` made `area:OR-tools` a HALT, - // rejecting a legitimate label (round 7, minor 1). - if (value.includes(',') || /(^|\s)(AND|OR|NOT)(\s|$)/.test(value)) { - halt(`\`## Eligibility\` declares \`${value}\`, but the declaration takes exactly one label`) - } - if (MARKDOWN_BLOCK_MARKERS.some(marker => value.startsWith(marker))) { - halt( - `\`## Eligibility\` declares \`${value}\`, which is a copied markdown wrapper, not a bare label`, - ) - } - if (value.length > GITHUB_LABEL_CAP) { - halt( - `\`## Eligibility\` declares a ${value.length}-character value, longer than the host's label cap (${GITHUB_LABEL_CAP})`, - ) - } - if (value.split(/\s+/).filter(token => token.includes(':')).length > 1) { - halt(`\`## Eligibility\` declares \`${value}\`, which juxtaposes several labels on one line`) - } - // The guideline's SEPARATE content MUST, layered on top of the seven triggers rather than - // widening them: this value reaches an agent prompt, so it may never be a command fragment. - assertSafePromptText('Eligibility', value) + // The shape triggers plus the content MUST, in `policy-sections.ts` — the SAME rules each + // `## Workflows` routing key gets, because the schema states them once for every label slot. + assertLabelValue('`## Eligibility`', value) return value } @@ -216,7 +163,7 @@ function readEligibility(markdown: string, warnings: string[]): string | undefin function assertTierShapes(tiers: readonly string[]): void { for (const tier of tiers) { if (!isLabelShape(tier)) { - halt( + policyHalt( `\`## Auto-Advance\` names \`${tier}\`, which is not a well-formed \`family:tier\` label`, ) } @@ -238,19 +185,21 @@ function readAutoAdvance(markdown: string, eligibility: string | undefined): str const value = lines[0]! if (lines.length > 1) { - halt( + policyHalt( `\`## Auto-Advance\` carries ${lines.length} non-empty lines, but takes exactly one switch`, ) } if (value === AUTO_ADVANCE_OFF) return value if (/\b(AND|OR|NOT)\b/.test(value)) { - halt(`\`## Auto-Advance\` declares \`${value}\`, but the switch is a tier, not an expression`) + policyHalt( + `\`## Auto-Advance\` declares \`${value}\`, but the switch is a tier, not an expression`, + ) } const tiers = value.split(',').map(tier => tier.trim()) assertTierShapes(tiers) const foreign = tiers.filter(tier => tier !== eligibility) if (foreign.length > 0 || new Set(tiers).size !== tiers.length) { - halt( + policyHalt( `\`## Auto-Advance\` declares \`${value}\`, which is not this project's \`## Eligibility\` ` + `tier (${eligibility ?? 'none declared'}) — a tier outside eligibility is never selected, ` + `so it could never advance`, @@ -321,13 +270,13 @@ function readStopPredicate(markdown: string): { predicate?: string; maxIteration // Named separately from "matches neither grammar": an ASCII arrow is a spelling mistake with // an obvious fix, and reporting it as an unrecognised line sends the maintainer hunting. if (ASCII_ARROW.test(line)) { - halt( + policyHalt( `\`## Stop Predicate\` line \`${line}\` uses \`=>\`, but the documented arrow is \`⇒\` ` + `(U+21D2) — the same form the fan-out workflow requires, so the two realizations of the ` + `loop read this file identically`, ) } - halt( + policyHalt( `\`## Stop Predicate\` line \`${line}\` matches neither \`\` nor \`max-iterations: \``, ) } @@ -360,7 +309,7 @@ function assertSelector(selector: string, line: string): void { if (selector === 'root') return const payload = /^(?:tag|type):(.*)$/.exec(selector)?.[1] ?? '' if (payload.length === 0) { - halt( + policyHalt( `\`## Stop Predicate\` line \`${line}\` has an empty selector payload — \`tag:\`/\`type:\` needs a label`, ) } @@ -377,7 +326,7 @@ function assertCondition(condition: string, line: string): void { const parts = condition.split(/\s+and\s+/i).map(part => part.trim()) const valid = parts.every(part => CONDITIONS.includes(part) || /^has-tag:\S+$/.test(part)) if (!valid) { - halt( + policyHalt( `\`## Stop Predicate\` line \`${line}\` names \`${condition}\`, which is not a canonical macrostate (${CONDITIONS.join(', ')}) or \`has-tag: