This file is the rolling list of AH's product-level requirements. Each entry below is a self-contained R-NNNN — a single feature / capability of the extension, with rationale and decisions captured at the time it was introduced. R-NNNN ids are append-only and never recycled; declined requirements stay in place with a strikethrough and a DECLINED in vX.Y.Z marker (see R-0002).
Heads-up — do not confuse with the consumer-side file. This is
REQUIREMENTS.mdof the AH repo itself, listing AH's own product requirements. The companion file.pi/REQUIREMENTS.md, created inside each consumer project (per R-0006 from v0.9.0), is a different document with the same shape but a different scope: it lists the consumer project's requirements (the things the dev is building with AH), not AH's own. Keep the two cleanly separated when editing.
AH is a PI extension and can be installed with the pi install command.
Decisions:
- Channel: Git (GitHub) — install via
pi install git:github.com/Skillbill/agentic-harness[@<ref>]. No npm publish in v1. - Identity:
name = "@skillbill/agentic-harness"inpackage.json(scoped to the Skillbill org). - PI layout: convention dirs —
extensions/index.ts(entry),prompts/(slash commands),skills/(skills). TS helpers inlib/excluded from the convention dirs and reached via relative imports. - PI manifest: the
pifield ofpackage.jsonpoints explicitly atextensions/index.ts,skills,prompts(safety belt: auto-discovery does not promote.tsfiles inlib/to standalone extensions). - Peer dependencies:
@earendil-works/pi-coding-agentandtypebox(both provided by PI, not bundled by AH — see PI'sdocs/packages.md).
Status: declined. Implemented in v0.1.0 (PR #1) + v0.2.0 (PR #2) + v0.3.0 (PR #3 fix), then removed in v0.5.0.
Rationale: PI v0.74.0 natively displays a
Package Updates Availablebanner at startup when an installed package has a new upstream ref — see screenshot in the cleanup PR. AH's custom OTA duplicated this notification with slightly richer UX (interactive modal, automaticctx.reload()) but:
- added ~280 lines across the OTA module, install-info, version-reader, cache I/O, and dialog;
- introduced maintenance edge cases (stale cache, network errors, invalid
pi updateflags, install-path detection, pinning detection);- displayed the prompt in competition with the native PI banner, creating visual noise;
- relied on
ctx.reload()with subtle behavior (terminal-only, loses in-memory state) and on thepi updatesubprocess.Decision: removed all OTA code. AH delegates entirely to PI's native mechanism. The user runs
pi update(orpi update --extension git:...) at will, from the terminal, outside the pi session.Consequences for the user:
- On unpinned install: PI shows the banner; a manual command updates the package. No automatic reload — the user restarts
pito load the new code.- On pinned install (
@vX.Y.Z): PI follows its standard semantics (pi updateskips pinned packages). To upgrade:pi installwith the new ref.See
CLAUDE.md§ How to release a new version and § Install scopes for the updated operational flow.
Starting from v0.7.0 AH adopts a formalized release workflow and a consumer migration mechanism applied automatically at PI's session_start. Goal: when a consumer project upgrades AH (e.g. v0.7.0 → v0.8.0 via pi update), AH must be able to bring the project state in line with the new version without any manual intervention from the dev.
Decisions:
- CHANGELOG.md at root, Keep a Changelog 1.1.0 format, SemVer. Standard sections + a
Migrationsection per version (English, prose or pseudocode). Populated retroactively from v0.1.0. Shipped with the Pi Package (included inpackage.json#files). - GitHub Action
.github/workflows/release.yml: triggers onv*.*.*tags, extracts the matching section fromCHANGELOG.mdwith POSIXawk(zero deps) and creates the release viagh release create(preinstalled on GitHub-hosted runners). Nonpm install, no third-party actions — we minimize the supply chain. - Consumer marker:
<consumerRoot>/.pi/ah-version. Tolerant read: accepts plain text (0.7.0\n) or JSON{ "version": "x.y.z" }. Written by AH after every successful migration step (checkpoint). - Migration framework:
lib/migrate-consumer.ts(runner) +lib/migrations/index.ts(registry) +lib/migrations/types.ts(ConsumerMigrationcontract) +lib/migrations/v<M>_<m>_<p>.ts(entries). Single signature:apply(consumerRoot, pi) => Promise<void>. Application order: ascending semver, with filtermarker < target ≤ installed. - Idempotency: mandatory invariant — every
applymust be safe to re-run (mkdirSync(..., { recursive: true }),if (!existsSync) ..., rename only if source exists and target does not). - Git Safety Rule unchanged: migrations never run mutating git commands. They may mutate files under
.pi/and in the working tree, but staging/commit/push remain the dev's responsibility. - Failure non-blocking: error on a migration → marker stays at the last successful step, error printed, AH keeps loading. The user fixes and relaunches the session.
- Hook: registered inside the existing
session_starthandler ofextensions/index.ts, not inbefore_agent_start. Rationale: the migration is a one-shot action per session;before_agent_startfires for every LLM turn (as confirmed by the existingcodebase-indexandcurrent-task-contexthandlers which re-detect at every turn) and is not the right layer. - Initial list: empty. v0.6.0 is the baseline; the framework exists and writes the marker but applies nothing. v0.7.0 keeps the list empty (framework shipped, no consumer-side change to apply yet). The first real migration arrives with v0.8.0.
Update (v0.8.0): the first real consumer migration ships in lib/migrations/v0_8_0.ts — an idempotent rename of the 5 Italian-named codebase docs to English (INTEGRAZIONI.md → INTEGRATIONS.md, etc.), plus rewrite of references in INDEX.md and keys in .cache.json. This confirms the framework works end-to-end: the marker advances, the rename is idempotent, no git mutations are emitted, and failures remain non-blocking.
Consequences for the AH dev (see CLAUDE.md § How to release a new version and § Consumer migration):
- Every release requires a
CHANGELOG.mdupdate before tagging. - Compatibility steps that touch the consumer filesystem live as migration code, not as textual instructions in the changelog (though they must still be documented in the corresponding
Migrationsection). - A pushed tag triggers the GitHub Release automatically: no need to create the release by hand in the GitHub UI.
Status: decommissioned (v0.24.0). The
session_startcompat check (lib/check-pi-compat.ts) and itsextensions/index.tshook were deleted. Reason: PI ships on a0.xversioning scheme, so the^X.Y.Zcaret range that AH declares anchors on the minor (^0.78.0⇒>=0.78.0 <0.79.0). Every PI minor bump therefore fell outside the range and re-raised a false-positive[ah-pi-compat-warning], which could only be silenced by cutting a no-op AH release that bumpedpeerDependencies(see the v0.15.2 / v0.23.2 / v0.23.3 entries below — three such releases). The recurring user-facing noise plus the busywork releases outweighed the diagnostic value: a genuine API mismatch already surfaces as a runtime error, andpeerDependenciesis retained as honest npm metadata regardless. The original requirement is preserved below for historical context; none of it is live code anymore.
Starting from v0.8.0 AH verifies at session_start that the running PI version satisfies the range declared in package.json#peerDependencies["@earendil-works/pi-coding-agent"]. On a mismatch the user in the consumer project is clearly notified; AH still keeps loading commands/tools/hooks (warn loud, non-blocking).
Rationale: PI v0.74.0 does not validate an extension's peerDependencies on its own. Without this check, an AH version that requires APIs newer (or older) than those actually exposed by the installed PI only fails at runtime — typically in the middle of an LLM turn, with an opaque stack trace. The check moves the failure from "silent crash mid-task" to "visible warning at first session_start".
Decisions:
- Source of truth:
peerDependencies["@earendil-works/pi-coding-agent"]in AH'spackage.json. No dedicatedpi.compatibilityfield: the range is already there, it's already the standard npm/Pi convention, and the AH dev updates it on every release that adopts new PI APIs anyway. - Runtime source for the PI version: named export
VERSIONfrom@earendil-works/pi-coding-agent(see PI v0.74.0'sdist/config.d.ts). No lookup viagetPackageJsonPath()— fewer hops. - Minimal in-house semver matcher (
lib/check-pi-compat.ts:satisfies): supportsX.Y.Z(exact),^X.Y.Z,~X.Y.Z,>=X.Y.Z. More exotic ranges (e.g.0.74.x || 0.75.x) returnnull→ skip + diagnostic warn ("range not recognized"). Consistent with AH's zero-deps philosophy (see also theawk-onlyrelease.ymlfor R-0003): nosemverpackage added. - Warn loud, non-blocking: on a real mismatch AH emits a warning on three channels simultaneously and continues loading.
console.warn— consistent with the existing[agentic-harness] …logging.ctx.ui.notify(msg, "warning")— toast in PI's TUI footer (guarded byctx.hasUIbecause in print/RPC mode no UI exists).pi.sendMessage({ customType: "ah-pi-compat-warning", content, display: true })— persistent message in the session scrollback; it does not disappear. The triple coverage is intentional: a user who ignores the toast still finds the message scrolled above, and someone launching in non-TUI mode still sees theconsole.warn.
- Never blocking: even on a severe mismatch AH keeps registering prompts/tools/hooks. The decision to "stop" is left to the user, who — once the warning is visible — chooses whether to fix the version or proceed. Consistent with the non-blocking policy of
migrateConsumer(R-0003). - Never throws: the module self-isolates with try/catch; the
session_starthandler inextensions/index.tsalso has its own safeguard try/catch. - Hook: registered inside the existing
session_starthandler ofextensions/index.ts, beforemigrateConsumer. Rationale: if PI is too old, the migration might use missing APIs and crash before the compat warning is seen.
Consequences for the AH dev:
- Update
peerDependencies["@earendil-works/pi-coding-agent"]before tagging every time newer PI APIs are adopted. The check protects the user only if this field is honest. - If a more exotic range than those supported is needed, first extend
satisfies()inlib/check-pi-compat.ts(and the relevant verification checklist), then tag.
Out of scope: extending the check to typebox (the other peerDep). typebox arrives from PI as a transitive dep and is not practically replaceable by the user — a mismatch indicates a PI-side problem, not a consumer-side one.
Starting from v0.8.0 AH's authoring language is English across the board: prompts, skills, top-level docs (WORKFLOW.md, task-layout.md, REQUIREMENTS.md, CLAUDE.md, CHANGELOG.md), console logs, and migration description fields. The natural language of the content generated by AH inside a consumer project (the body of TASK.md, DISCUSS.md, PLAN.md, VERIFY.md, and the prose of .pi/codebase/*.md) is instead chosen by the consumer via a small config file committed at the project root.
Rationale: a single English source of truth is easier to maintain and review by non-Italian-speaking contributors and makes AH legible to the wider Pi ecosystem. At the same time, each consumer team retains the freedom to localize its own working documents — many of which are read by non-technical stakeholders who may prefer the team's working language.
Decisions:
- Config file:
<consumerRoot>/.pi/ah-config.json, JSON, committable. Shape:{ "configVersion": "1", "contentLanguage": "en" }configVersionis a string and is currently"1". It governs future schema evolutions of this same file; bump only on breaking-shape changes.contentLanguageis a free-form code (e.g."en","it","fr"). Unknown codes are not rejected: they are surfaced to prompts as-is, so the consumer can experiment with future languages without an AH code change.
- Default: when the file is unreadable (malformed JSON, wrong shape),
readAhConfigfalls back tocontentLanguage = "en". This is the new global default, replacing the implicit Italian default that existed up to v0.7.0. - Auto-creation (v0.8.1): when the file is absent, AH writes it at
session_startso the language choice is always explicit and committable. The value comes fromdetectConsumerLanguage: if at least 3 distinct unambiguous Italian function words (della,degli,perché,sono, …) appear anywhere in the existing.pi/codebase/*.mdcontent (excludingINDEX.md), the file is created with"contentLanguage": "it"— preserving the voice of legacy v0.7.x consumers whose docs were authored under the old implicit-Italian default. Otherwise (new consumers, empty.pi/codebase/, English-looking content) the file is created with"contentLanguage": "en". The write is atomic (tmp + rename), never throws, and silent on failure beyond a singleconsole.warn. Once the file exists, AH never overwrites it — a manual edit (or a commit with a different value) is the source of truth. - Filenames are never localized:
TASK.md,DISCUSS.md,PLAN.md,VERIFY.md,INDEX.md, and the 7 codebase docs keep the exact names AH defines, in every locale. Only the prose inside these files honorscontentLanguage. - Error handling:
readAhConfignever throws. Missing file, invalid JSON, wrong shape, permission errors — every failure path downgrades to a singleconsole.warnand returns the default config. AH always boots. - Surface to prompts:
lib/register-prompt.tssubstitutes two new placeholders in every prompt body —$CONTENT_LANG(display name, e.g."English","Italian") and$CONTENT_LANG_CODE(raw code, e.g."en","it") — and every shipped prompt / skill carries an**Output language**: ... MUST be written in **$CONTENT_LANG**directive so the LLM honors the consumer's choice when generating content. - Visibility: the resolved language is logged once at
session_startinextensions/index.ts(🌐 Content language: English (en)), so the dev sees immediately what AH thinks the project's working language is.
Implementation pointers:
lib/ah-config.ts— typed reader, default fallback, singleconsole.warnon any failure.lib/register-prompt.ts— placeholder substitution for$CONTENT_LANG/$CONTENT_LANG_CODE.extensions/index.ts— callsreadAhConfigatsession_startand emits the🌐 Content language: …log line.lib/migrations/v0_8_0.ts— consumer-side rename of the 5 Italian-named codebase docs (INTEGRAZIONI.md,ARCHITETTURA.md,STRUTTURA.md,CONVENZIONI.md,CRITICITA.md) to their English counterparts (INTEGRATIONS.md,ARCHITECTURE.md,STRUCTURE.md,CONVENTIONS.md,TECHNICAL_DEBT.md), with rewrite of.pi/codebase/INDEX.mdreferences and.pi/codebase/.cache.jsonkeys.
Consequences for the AH dev:
- New prompts, skills, docs, and migration
descriptionstrings must be authored in English. Don't reintroduce Italian. - When adding a placeholder substitution beyond
$CONTENT_LANG/$CONTENT_LANG_CODE, mirror the same defensive style (substitution, not parsing): the prompt body remains the spec. - The default
contentLanguage = "en"is a behavioral default, not a policy: a consumer is free to commit.pi/ah-config.jsonwith"contentLanguage": "it"and expect AH-generated content to stay in Italian.
Starting from v0.9.0 AH manages a single document <consumerRoot>/.pi/REQUIREMENTS.md listing the consumer project's requirements as R-NNNN entries. The list is treated as an input to the task workflow: it is read by /ah:task-discuss, /ah:task-plan, and /ah:task-verify, and is mutated only by /ah:task-new (step 2-bis — link to existing R-NNNN, create one inline, or skip) and /ah:task-discuss (step 7.5 — new / amend / no change post-discussion). It is never harvested at /ah:task-done — that phase has no opinion on requirements.
Decisions:
- Location:
<consumerRoot>/.pi/REQUIREMENTS.md(single file, never split). Sibling of.pi/codebase/,.pi/tasks/,.pi/ah-config.json— under AH's.pi/namespace, not in repo root. - Format: frontmatter (
project,status: living,created,updated) + sectionsContext/Requirements/Out of scope/Historicized decisions/Release history. Each R-NNNN entry: 1–3 sentence body +**Rationale**line +**Linked tasks**line maintained automatically. R-NNNN ids are monotonic, zero-padded to 4 digits. - Filename invariance: always
.pi/REQUIREMENTS.md, never localized. Body prose is in$CONTENT_LANG(per R-0005); identifiers, frontmatter keys, and section headings stay English/ASCII. - Bootstrap: the v0.9.0 consumer migration (
lib/migrations/v0_9_0.ts) drops an empty skeleton in.pi/on first session after upgrade. Idempotent. No dedicated bootstrap command — R-NNNN entries grow organically through/ah:task-newand/ah:task-discuss. - Linking:
TASK.mdfrontmatter gains an optionalimplements: [R-NNNN, ...]key (list,^R-\d{4}$). Set by/ah:task-new(single id or empty), optionally extended by/ah:task-discuss. AH appends the task id to the R-NNNN's**Linked tasks**line; reverse cleanup on rename/removal is the dev's responsibility (tolerant, never destructive). - Context loading: skill prompts read REQUIREMENTS.md directly when needed (discuss / plan / verify). Not added to
context-needed:inPLAN.md— that list remains scoped to the 7 codebase docs. Not injected atsession_starteither: the per-skill Read pattern is sufficient and avoids token waste in unrelated turns. - Git Safety widening: the existing
/ah:task-newgit-auto-commit exception is widened to allow.pi/REQUIREMENTS.mdin the same commit, only when step 2-bis touched it; the/ah:task-discussexception is widened identically for step 7.5. No other prompt or skill mutates the file.
Consequences for the AH dev:
- Any new prompt/skill that operates on a task and needs intent context should read
.pi/REQUIREMENTS.mddirectly, mirroring the §3-bis pattern in discuss/plan and the DoD-Requirements subsection in verify. - Do not extend
context-needed:to cover REQUIREMENTS — keep the two namespaces (codebase docs vs project intent) distinct. - When changing the file's structure (sections, frontmatter keys, R-NNNN body fields), update
templates/REQUIREMENTS.md, the v0.9.0 migration's substitution logic, and the parsing rules inprompts/task-new.mdstep 2-bis andskills/ah-task-discuss/INSTRUCTIONS.mdstep 7.5 together.
Starting from v0.10.0 every TASK.md carries a priority: key in its frontmatter, a coarse-grained urgency tag with exactly four levels (LOW, NORMAL, HIGH, IMMEDIATE, uppercase, case-sensitive). The default is NORMAL. The field is consumed by /ah:project-status to render a per-task priority marker and to sort the Backlog section in priority-descending order; it is not consumed by the inner-cycle phases (discuss / plan / execute / verify) — priority is about queue ordering, not work content.
Decisions:
- Allowed values & casing: exactly the four uppercase tokens above. Anything else (missing, blank, lowercase, typo) is normalized to
NORMALby readers; the migration only writes the canonical uppercase form. - Default on creation:
NORMAL. From v0.23.0,/ah:task-newstep 3-ter asks the dev once (optional,skipkeeps the default), so urgency can be front-loaded at creation alongside the customer / project metadata (R-0017). The interview remains a single compact prompt — no clarification turn — and the field stays editable by hand afterwards. Earlier behavior (v0.10.0 – v0.22.0):/ah:task-newnever asked; the template's literalpriority: NORMALwas written and the dev edited the file by hand for any non-default value. The original rationale (priority is queue-management metadata that evolves over time, separate from defining the task) is still honored — the dev can still ignore the question and bump the value later — but symmetry with the customer/project step in R-0017 and explicit user feedback ("voglio che mi venga chiesto la priorità") moved the default from "always-skip" to "ask-once-optional". - Display in
/ah:project-status: a single-character marker in column 2 of the row prefix (!= IMMEDIATE,^= HIGH,= NORMAL,v= LOW), uniform across all sections so columns align. Column 1 stays the current-task marker (▶). - Backlog ordering:
IMMEDIATE → HIGH → NORMAL → LOW, tie-break byidascending. Other sections keep their existing order (in-progress / review / recently-closed have their own natural sort). - Bootstrap & migration: existing TASK.md files in any consumer that upgrades to v0.10.0 are walked by
lib/migrations/v0_10_0.tsand getpriority: NORMALinserted after thestatus:line. Idempotent — skips any file that already declares apriority:value. Non-destructive — never overrides an existing value (the dev owns it). - Linkage to other contracts: no impact on
implements:,progress:,branch:, the inner-cycle phases, or the codebase-doc workflow.WORKFLOW.mdis unchanged because the lifecycle states are still the same; only the per-task metadata grows by one field.
Consequences for the AH dev:
- When you add a new reader of TASK.md frontmatter, normalize the priority field the same way as
/ah:project-status(case-insensitive parse, defaultNORMALon miss). Don't add a fifth level without updating R-0007, the template, the migration, and the project-status renderer in lockstep. - The migration is append-only on missing fields; if you ever need to rewrite an existing priority (e.g. to normalize casing), add a new migration rather than retrofitting v0.10.0.
Starting from v0.12.0, AH registers three keyboard shortcuts via PI's pi.registerShortcut API. Each opens a TUI overlay scoped to a single task bucket and lets the dev cycle through the bucket's tasks with ↑ / ↓ and dismiss with ESC. The popup body shows the full TASK.md (frontmatter + sections) truncated to ~30 lines, plus the relative file path.
Decisions:
- Three shortcuts, one per bucket (rather than a single picker over all tasks): keeps each popup focused on the slice the dev is reasoning about — current work, next pick-up, or recent closures. Matches the same mental model as the three sections in
/ah:project-status.alt+p→in-progress/, sorted by id ascending.alt+k→backlog/, sorted byprioritydescending (IMMEDIATE → LOW) with id-ascending tie-break (mirrors/ah:project-statusBacklog).alt+c→done/, sorted byupdateddescending with id-descending tie-break (most recent closure first).
alt+bis deliberately not used — PI already binds it (andalt+left,ctrl+left) to the "move back one word" editor command.alt+kwas chosen as the next closest mnemonic for backlog.- Body truncation: hard cap of 30 content lines. Long bodies (typical for tasks with verbose
## Goalor## Definition of Done) trigger a… N more line(s) truncated — open <relPath> for full textfooter. Rationale: the popup is for the "fast glance" use case; full reading happens in an editor. - No runtime import of
@earendil-works/pi-tui: the popup component duck-types pi-tui'sComponentshape (render/handleInput/invalidate) and matches ESC / ↑ / ↓ from raw ANSI byte sequences. Avoids adding pi-tui topeerDependenciesfor a single overlay component. Side effect: only standard-mode escape sequences are recognized; Kitty CSI-u modifiers are out of scope (the three keys we care about all work in standard mode regardless). - Backward-compatibility on old PI:
pi.registerShortcutis called inside atry/catch. If the running PI is too old to support shortcut registration, AH logs a singleconsole.warnand continues — the rest of the extension stays functional. - Empty bucket UX: shortcut shows a
ctx.ui.notify("No tasks in .pi/tasks/<bucket>/", "info")toast and does not open an empty popup.
Consequences for the AH dev:
- Don't break parity with
/ah:project-statusBacklog ordering: both usesortForBucketfromlib/show-task.ts. Any change to the priority-rank function or tie-break rule must update both call sites consistently. - When adding a fourth shortcut (or moving keys), pick from the alt+letter combinations not reserved by PI's editor / app keymaps (
alt+b,alt+d,alt+f,alt+y,alt+v,alt+up,alt+enter,alt+left,alt+right,alt+delete,alt+backspaceare all taken — seecore/keybindings.d.tsin PI). - The popup is a single-page renderer (no intra-task scroll). If the body cap becomes too restrictive in practice, the right move is a paged Component with
pageUp/pageDown— not raising the cap, which would dwarf the chat area.
Starting from v0.13.0, AH ships a /ah:help slash command that opens a single-page TUI overlay with the installed AH version, the list of AH slash commands, and the keyboard shortcuts from R-0008. ESC closes. From v0.21.0, the slash-command section is interactive: ↑ / ↓ move the focus, ENTER prefills the editor with /<name> (the dev appends any arguments and presses ENTER again to run).
Decisions:
- Registered via
pi.registerCommand, not via a prompt file. The default registration path for/ah:*commands is theprompts/*.mdloop inextensions/index.ts, which routes the body throughpi.sendUserMessageso the LLM produces the response./ah:helpdeliberately bypasses that — the popup is purely local (overlay + raw filesystem read ofpackage.json) and never burns an LLM turn. - Command list is discovered, not hard-coded. The handler calls
pi.getCommands().filter(c => c.name.startsWith("ah:"))so the popup automatically reflects whatever set of prompts is currently registered (after/ah:standupwas dropped in v0.11.0, after/ah:helpitself was added in v0.13.0, etc.). The shortcut list, by contrast, is hard-coded — there is no public PI API to enumerate registered shortcuts. - Layout: title (
🆘 agentic-harness — help vX.Y.Z), separator, "Keyboard shortcuts" block, blank line, "Docs" line, separator, "Slash commands" selectable list (with▶marker on the focused row), separator, footer (↑/↓ select · enter run · esc close). - Prefill, not auto-dispatch. ENTER on a focused command writes
/<name>into the core input editor viactx.ui.setEditorTextand closes the popup. AH does not dispatch the slash command itself — several AH commands take arguments (e.g./ah:task-start <ID>), and surfacing the command in the editor lets the dev complete it before submitting. For args-less commands the dev just hits ENTER again. There is nopi.dispatchCommandAPI to call instead; even if there were, the prefill flow keeps the dev in the loop (visible in the editor, can be cancelled with backspace). - Reuse, not generalization:
lib/help-popup.tssits next tolib/task-popup.tsandlib/branch-switch-popup.ts. All three duck-type pi-tui'sComponent, recognize ESC from a raw ANSI byte, and uselib/popup-frame.tsfor the chrome. They shareclipToWidthconceptually but each owns its own copy — a real generalization (aBasePopupclass) isn't justified by three call sites. - View / action separation:
HelpPopupis side-effect-free — it owns keyboard handling and the visual marker, nothing else. The editor prefill happens in the shortcut handler inextensions/index.tsafter thedonecallback resolves, mirroring the posture ofBranchSwitchPopup(where the git checkout lives in the handler, not the component). - Fallback on missing UI: if
ctx.hasUIis false (RPC / print mode), the handler returns early after actx.ui.notify(when notify is available) instead of crashing. IfsetEditorTextthrows (older PI without the API surface), the handler emits a warning toast and stays alive.
Consequences for the AH dev:
- When you add or remove a
prompts/*.mdfile,/ah:helpupdates on its own — no doc churn here. - When you add a new keyboard shortcut in
extensions/index.ts, also append a row to the hard-codedshortcutRowsarray in the/ah:helphandler. Same applies if you change the key for an existing shortcut. - Do not add LLM-bound content to the popup body (no
$@,$EXT_DIR, orpi.sendUserMessagefrom inside the handler). The popup is meant to be cheap — a single overlay open / close cycle should not cost a token. - Don't move git / filesystem side effects into
HelpPopup: the component must stay side-effect-free so a future caller can swap the prefill action for a different one (e.g. auto-dispatch when PI grows that API) without touching the popup.
Starting from v0.15.0, AH ships a keyboard shortcut alt+s that opens a selector popup listing the default branch (main) plus every in-progress task with a branch: field in its TASK.md frontmatter. The dev navigates with ↑ / ↓, picks with ENTER, cancels with ESC. On selection AH runs git checkout <branch> — gated by a working-tree-clean check.
Decisions:
- List composition: the first row is always
main, hard-coded for now (TBD: detect the consumer's actual default branch viagit symbolic-ref refs/remotes/origin/HEAD). The rest are in-progress tasks (.pi/tasks/in-progress/) filtered by having a non-nullbranch:in frontmatter, sorted by id ascending — matches the order used by/ah:project-statusand thealt+ppopup. - Current branch annotation: the row matching
git branch --show-currentis suffixed with(current). Selecting it is a no-op — AH shows aAlready on <branch>toast and skips the checkout. Rationale: avoids runninggit checkouton the branch we're already on, which is a no-op for git but a confusing UX. - Clean-tree gate: before any
git checkout, AH runsgit status --porcelain. A non-empty stdout aborts the switch with a⚠ Working tree not clean — commit or stash before switchingwarning toast. The dev is expected to commit or stash and try again; AH does not offer to stash automatically (out of scope — git workflow is the dev's call). - Git Safety Rule exception: the shortcut is an explicit dev-triggered mutation, same posture as
/ah:task-newand/ah:do-git-stuff. The popup separates view (selection) from action (checkout): the component itself never touches git; only the shortcut handler does. - Error surfacing: every git-side failure (status, checkout) is reported through
ctx.ui.notify(message, "error" | "warning")— never thrown. AH stays alive and the popup is closed by the time we get there. branch: nullsemantics: a task whose frontmatter saysbranch: null(the default before/ah:task-start) is filtered out of the list — there's no branch to switch to. Same forbranch:missing entirely.
Consequences for the AH dev:
- The component (
lib/branch-switch-popup.ts) must remain side-effect-free: it owns the keyboard handling and the visual marker, nothing else. Adding git logic inside the component is a regression — keep it in the handler. - When you add a new candidate source (e.g.
review/task branches, recently-pushed remotes), update both the popup's list-building block inextensions/index.tsand the sort logic. Don't sneakawaitcalls insiderender()— the Component contract requires synchronous render. - If you ever change the clean-tree definition (e.g. tolerate untracked-but-ignored files), centralize it in a
lib/git-status.tshelper; right now the check is a one-liner inline because it has a single call site.
Every keyboard shortcut AH registers via pi.registerShortcut(...) MUST be listed in the shortcutRows array rendered by the /ah:help (and alt+h) overlay. The rows in the popup are in registration order — same order the dev encounters the shortcuts in extensions/index.ts.
Decisions:
- Hard-coded, not enumerated: PI exposes no runtime API to enumerate registered shortcuts (
pi.getCommands()only covers slash commands). The list lives next to thepi.registerShortcut(...)calls; a comment on both sides cross-references R-0011 so future contributors know to keep them in sync. - One-line format per row:
<key> <emoji> <label> (<hint>). The emoji mirrors the popup title's icon (📋, 📥, ✅, 🔀, 🆘) for visual continuity. Wide-char alignment is handled bylib/popup-frame.tsvia the emoji-awarevisibleWidth(R-0008 fix in v0.14.1). - Coverage scope: shortcuts only. Slash commands continue to be discovered dynamically via
pi.getCommands()in the same popup — that part is auto-maintained. - Enforcement: convention + review. There's no test that diffs the registered set against
shortcutRows. The cost of a missed entry is low (the dev's shortcut works but is invisible in/ah:help), so a runtime check isn't justified.
Consequences for the AH dev:
- When you add or rename a shortcut: edit the
pi.registerShortcut(...)call AND the correspondingshortcutRowsline in the same commit. The two changes belong together — a commit that splits them is a bug bait. - When you remove a shortcut: delete both lines. Leaving a stale row in
shortcutRowslies to the user. - If a new PR adds a shortcut without touching
shortcutRows, the right reviewer comment is "missing R-0011 entry in/ah:help" — pointing at this requirement makes the fix obvious.
Starting from v0.16.0, the codebase-map logic lives in procedures/map-codebase.md and is not exposed as the /ah:map-codebase slash command anymore. The 1015-line procedure is referenced via $EXT_DIR/procedures/map-codebase.md by every consumer that needs to (re)generate .pi/codebase/.
Decisions:
- No auto-registration:
extensions/index.tsscansprompts/*.mdfor slash-command registration. By moving the file toprocedures/we make AH's "is this a command?" decision a function of location, not of a frontmatter flag — there's nothing to remember, no opt-in/opt-out marker that can be forgotten. procedures/is for inline sub-procedures: this directory was already listed inCLAUDE.mdas a sibling oftemplates/referenced by prompts/skills via$EXT_DIR, but it was empty. R-0012 establishes the convention: anything that is executed inline by another phase (1+ caller, no standalone user value) belongs inprocedures/; anything the dev types directly belongs inprompts/.- Four call sites, one source:
skills/ah-task-discuss,skills/ah-task-plan,skills/ah-task-execute, andprompts/task-done.mdall read$EXT_DIR/procedures/map-codebase.mdand execute its steps 2–5. The dev never types the command — they hit/ah:task-discuss(or similar), and if.pi/codebase/is missing the phase proposes running the procedure inline. - Manual refresh path: if a dev wants to regenerate the map outside the task cycle, they ask the agent in chat ("run the codebase-map procedure" or similar). AH reads the procedure file and runs it. No slash command, no shortcut — chat is the only entry.
- Why not a frontmatter
internal: trueflag: the alternative was to keep the file inprompts/and filter it out of the auto-discovery loop based on a frontmatter marker. Rejected because: (a) it'd add a one-off code path inregister-prompt.tsfor a single file, (b) prompts and procedures have different audiences (dev vs. agent), and a folder separation makes that explicit, (c) the existingprocedures/directory was already declared inCLAUDE.mdwaiting for content.
Consequences for the AH dev:
- When you introduce a new long sub-procedure that other phases reference (security audit, dependency scan, license check, …), drop it in
procedures/from day one. Do not put it inprompts/then refactor. - When you edit
procedures/map-codebase.mdand renumber the steps, search-and-update the foursteps 2–5references in the consumers (grep -rn "procedures/map-codebase" --include='*.md'). The references are step-number-coupled — splitting that coupling would mean changing the procedure layout. - Do not silently re-introduce
/ah:map-codebaseas a slash command. If a future requirement says "the dev wants a one-line manual refresh", prefer a different name (e.g./ah:refresh-codebase) that's a wrapper whose body isread $EXT_DIR/procedures/map-codebase.md and run it. Keeps the procedure file the single source of truth and the slash command minimal.
Starting from v0.17.0, when the consumer migration framework (R-0003) advances <consumerRoot>/.pi/ah-version and the resulting working-tree change is only that marker bump, AH stages and commits the file on the consumer's behalf. Without this exception every pi update produced a stray modification the dev had to clean up by hand at next session start — a paper cut the user explicitly asked us to fix.
Decisions:
- Trivial-state gate, not policy switch: the auto-commit fires only when all of these hold (parallel check in
autoCommitMarkerBump):- The consumer is inside a git working tree (
git rev-parse --is-inside-work-tree). - The current branch is
mainormaster. Feature branches are deliberately excluded — committing the marker bump on a feature branch pulls AH's housekeeping into that feature's history and produces a merge conflict when the feature lands. git status --porcelainreturns exactly one line, and its path is.pi/ah-version. Any other state (multiple files, different path) means a real migration touched the tree (e.g. v0.10.0 retrofittingpriority:into everyTASK.md) or the dev has work in progress — both demand human review.
- The consumer is inside a git working tree (
- Single commit shape:
chore: bump AH consumer marker to vX.Y.Z. Hard-coded message, noCo-Authored-By:trailer. Versioned identically to the installed AH version sogit log --grep "consumer marker"is a clean audit trail. - No push: the dev keeps push authority. Auto-commit is local-only.
- Never throws, always non-blocking: every git failure (no git on
$PATH, non-zero exit, detached HEAD, hook failure, pre-commit reject) is downgraded to a singleconsole.warnand session start continues. The marker file remains modified on disk so the dev can still commit it manually. - Branch detection is best-effort: we accept
mainandmasterto cover the two near-universal defaults without paying the round-trip togit symbolic-ref refs/remotes/origin/HEAD(which also requires a remote). Consumers with a custom default branch fall back to the v0.16.x behavior (manual commit) — they can rename tomainif they want the auto-commit. This is a deliberate trade-off: covers ~99% of real consumers, zero latency, zero remote chatter. - Explicit exception, not policy reversal: R-0003 still says "no git mutations from consumer migrations". R-0013 carves out one surgical case — the marker file written by the framework itself. Real migrations (
lib/migrations/v*.ts) keep the original prohibition; they may touch consumer files but never callgit. Same exception model used by/ah:task-new(auto-commit of the new TASK.md).
Consequences for the AH dev:
- Do not weaken the trivial-state gate to cover "messy but the marker is in there somewhere" cases. The single-file check is the only thing that prevents AH from accidentally committing a dev's WIP. A dirty-tree auto-commit would turn into the worst kind of footgun (lost diffs, stray commits, unhappy users).
- A future migration that legitimately wants to commit something must declare its own exception — do not reuse this auto-commit path. It's scoped to the marker only.
- When you add a new migration
vX.Y.Zthat touches consumer files (rename, frontmatter retrofit, etc.), expect the gate to not fire on that update: the working tree will contain.pi/ah-versionplus the files the migration changed. The dev sees both, reviews, commits together. This is by design. - If the consumer's default branch is anything other than
main/master(develop,trunk, …) the auto-commit silently skips. That's intentional — adding more defaults to the allowlist is a one-line edit when a real consumer asks for it. Don't widen speculatively.
Starting from v0.18.0, AH ships with a LICENSE file containing the full text of PolyForm Noncommercial 1.0.0 and package.json#license set to the SPDX identifier PolyForm-Noncommercial-1.0.0. Until v0.17.x the package.json declared MIT without a matching LICENSE file — an orphan declaration.
Decisions:
- PolyForm Noncommercial 1.0.0, not MIT / Apache / GPL: the intent expressed by Skillbill is "anyone can use and modify AH unless they make money from it". That intent does not match any OSI-approved license (the OSI definition requires unrestricted commercial use). PolyForm Noncommercial is the modern, software-tailored, lawyer-drafted (Heather Meeker et al., 2020) license that says exactly this. SPDX-recognized.
- Not Creative Commons BY-NC: CC explicitly recommends against using its licenses for software. PolyForm is the software-specific analog.
- Not "Commons Clause" rider on top of a permissive license: that pattern (used historically by Redis, MongoDB) is now considered convoluted and legally murky compared to a single-document license like PolyForm.
- Not BUSL (Business Source License): BUSL allows commercial use except for a specific competing-product carve-out and converts to OSI-licensed after a delay. Heavier than what's needed here, and changes shape over time.
- No automatic conversion to permissive after N years: AH stays PolyForm-NC indefinitely. If/when the policy changes, a new release with a different
LICENSEis the migration path —git logkeeps the historical license attached to historical tags. - Skillbill internal use is unaffected: the licensor (Skillbill) holds the copyright and the license restricts licensees, not the licensor. Skillbill's internal commercial use of AH is implicit and unrestricted.
- "Use" of AH inside a consumer project is non-commercial use of AH, not commercial: a paid developer running AH against a commercial codebase is using AH as a tool (the same way they'd use VS Code or an OS), not redistributing or selling AH. PolyForm Noncommercial covers that case via the "noncommercial purpose" + "fair use" sections. Distribution / resale of AH as a paid product is the case that requires a separate commercial license.
Consequences for the AH dev:
- Source files / docs may carry SPDX headers if added later, but the canonical statement is
package.json#license+LICENSEat repo root. Don't add per-file copyright headers retroactively — they age badly. - When merging external contributions: the contributor agrees, by submitting, that their changes are licensed back to Skillbill under PolyForm Noncommercial (documented in
README.mdContributing section). No CLA file required for now; revisit if/when contribution volume justifies one. - Changing license at a future release is a significant decision — it would warrant a major bump (e.g.
v1.0.0→v2.0.0with a differentLICENSE). Old releases keep their original license attached to their git tag. README.mdcarries a one-paragraph plain-English summary of the license so a casual visitor doesn't have to parse the legal text. Keep that summary truthful — if you change the license, change the README in the same commit.
R-0015 — Project-status progress derived from inner-cycle artifacts, not from the progress: frontmatter
Starting from v0.19.0, /ah:project-status computes the per-task percentage and inner-cycle phase the same way for every task in in-progress/ — current task and others. The numbers come from inspecting PLAN.md + steps/NN-*.md (with their frontmatter status: field) and the presence of DISCUSS.md / VERIFY.md. The progress: frontmatter field on TASK.md is no longer the source of truth — it remains as a last-resort fallback when no artifacts at all can be reached.
Why this change:
- The
progress:field was a dead letter. Agrep -rn "progress:" skills/ prompts/across the AH source shows that no phase writes it incrementally — onlypr-openandtask-doneset it to100. So every in-progress task that wasn't the current one rendered as0%until it reached PR. The deeper the task, the more the bar lied. - Inner-cycle artifacts already carry the right signal: each
steps/NN-*.mdhas astatus: todo | doing | done | blocked | failedfield that the agent is supposed to bump as it works through them. Reading those gives an honestdone/totalratio per task without any new write path.
Decisions:
- Single computation path for all in-progress tasks: drops the v0.18.x branch that treated "current task" differently. Same logic everywhere. Avoids the surprise where T-019 with 1/8 steps done renders as
[execute 1/8]from inside its branch but as0% [-]frommain. - Artifact source resolution order: (a) on-disk files if the feature branch is currently checked out, (b)
git show <branch>:<path>on the local branch otherwise, (c)git show origin/<branch>:<path>if the local branch is missing, (d) fall back to the on-disk file under the task dir as a last resort. Mirrors the existing TASK.md resolution flow from step 1 of the prompt. - New phase indicator
[no plan]: when the task dir contains onlyTASK.md(noDISCUSS.md, noPLAN.md, nosteps/), surface "task started but inner cycle bypassed" explicitly. Catches the case where/ah:task-startwas run and then the dev did freeform implementation commits withouttask-discuss/task-plan/task-execute. The bar stays at 0% — honest, because AH has no plan to measure against. - Fallback
[?]: when even artifacts can't be loaded (branch missing locally and on origin, disk only hasTASK.md,progress:is null), show[?]to signal "cannot determine". Different from[no plan]:[no plan]is a known state (the dev bypassed the cycle),[?]is an unknown state (the data is unreachable from this checkout). - DoD checkboxes no longer drive
pct: in v0.18.x the current task's percentage came from## Definition of Donecheckbox count. That metric conflates "ready to close" (DoD) with "how far into execute" (steps). The new logic puts each in its own lane —pct= step progress, DoD readiness becomes visible only through theverifyphase appearing. - No write side-effects:
task-executeis not changed to updateprogress:after each step. Side-effectingTASK.md's frontmatter every commit would create merge-conflict noise on long-running tasks. Computing at read time keeps the file boring.
Consequences for the AH dev:
- The
progress:frontmatter key now has a single legitimate writer set:pr-open(→100) andtask-done(→100). If you ever consider writing it from another phase, prefer extending the read-time computation inproject-status.mdinstead. The field is kept (with its two writers) only as the documented fallback when artifacts can't be reached. - Step files must carry a
status:line pertask-layout.md§3.4. AH's plan templates already enforce this; if you add new step authoring paths, mirror the contract. - The
[no plan]indicator is informational —/ah:project-statusdoesn't gate or warn on it. If you want to enforce "tasks must follow the inner cycle", add a separate/ah:auditcommand; don't bolt enforcement onto the status renderer.
Starting from v0.20.0, AH exposes /ah:project-bootstrap, a guided
one-shot command that turns a brand-new project (no code yet, perhaps a
README / vision doc) into a workable AH state in a single session.
The command produces a populated .pi/REQUIREMENTS.md (## Context
- a first set of
R-NNNNentries with Rationale), three intent-based.pi/codebase/*.mddocs (STACK.md,ARCHITECTURE.md,CONVENTIONS.mdwritten from planned stack/architecture rather than from source), and a first set of 3–7 backlog tasks each wired to the new requirements viaimplements:. Everything is committed in a singlechore(bootstrap): …commit onmain/master.
Why this exists: until v0.19.x AH implicitly assumed brownfield
consumers. The inner cycle (task-discuss step 2) blocked when
.pi/codebase/ lacked ARCHITECTURE.md / STRUCTURE.md /
CONVENTIONS.md, and the only proposed remediation —
map-codebase — explicitly recommended skipping itself on greenfield
projects. So a dev installing AH on a brand-new repo had no on-ramp:
the v0.9.0 migration scaffolded .pi/REQUIREMENTS.md empty, but every
subsequent step required artifacts that only existed on a populated
codebase. R-0016 closes the loop by letting the bootstrap itself
produce those artifacts from project intent.
Decisions:
- Single command, not a phase:
/ah:project-bootstrapis not part of the inner cycle (discuss → plan → execute → verify). It precedes the first/ah:task-new//ah:task-startand is meant to run once at project genesis. Idempotency is not a goal — the command refuses to run if any of the three pre-flight signals (existing.pi/codebase/content, existing R-NNNN entries, existing task directories) is positive. - Five phases inside one command: pre-flight + raw-material read
→ vision/context (Phase A, free loop) → requirements harvesting
(Phase B, one R-NNNN at a time) → technical intent (Phase C,
3 mini-questions covering stack/architecture/conventions) → initial
backlog (Phase D, batch proposal with
all/pick/edit/drop) → write all files (Phase E) → commit (Phase F) → final output. The separation keeps the dialogue focused while the dev canstop/enoughat any phase boundary. - Intent-based codebase docs:
STACK.md,ARCHITECTURE.md,CONVENTIONS.mdare written with asource: intentfrontmatter flag plus a<!-- intent:keep --> … <!-- /intent:keep -->HTML-comment marker around substantive planned-architecture paragraphs. The marker is the contract withmap-codebase: when real code arrives and the procedure regenerates a stale doc, it preserves the marked blocks verbatim and adds a new## Observed statesection at the end. Thesource:field flips tointent+observed. No other doc is produced at bootstrap —STRUCTURE.md/INTEGRATIONS.md/TESTING.md/TECHNICAL_DEBT.mdemerge from real code and are populated by the first post-bootstrapmap-codebaserun. - Initial backlog must be ≥ 1 task with
implements:≠[]: a bootstrap that produces zero linked tasks adds no actionable next step. The dev can still answercancelin Phase D and accept zero tasks — in that case the command still writes REQUIREMENTS + codebase docs and commits, and the dev is expected to run/ah:task-newimmediately after. - Declared Git Safety exception, scope-bounded: same exception
pattern as
/ah:task-new(R-0006 widening for REQUIREMENTS.md) — the command MAY rungit add/commit/pushbut only for the exact paths it produced (the union of.pi/REQUIREMENTS.md, the 4.pi/codebase/*.mdfiles, and the N.pi/tasks/backlog/T-NNN-*/ TASK.mdfiles). Any unrelated dirty path aborts the auto-commit. - No new helper in
lib/: the prompt body is the spec, per the CLAUDE.md architectural note ("the body is the spec for the command"). The bootstrap reuses the textual patterns ofprompts/task-new.md(git safety exception, ID assignment, template substitution, R-NNNN insertion) and ofprocedures/map-codebase.md(forbidden files list). No TypeScript changes. task-discusssoftening (step 2): as a defensive side-effect,skills/ah-task-discuss/INSTRUCTIONS.mdstep 2 is loosened to require onlyARCHITECTURE.md+CONVENTIONS.md(was: + alsoSTRUCTURE.md). On a freshly bootstrapped projectSTRUCTURE.mdlegitimately does not exist yet; making it advisory rather than blocking lets the inner cycle proceed on the very first task. Also step 2 now recognizes thesource: intentfrontmatter so the agent anchors questions to planned architecture rather than observed code.- No new consumer migration: bootstrap is opt-in. v0.9.0 already
scaffolds an empty
.pi/REQUIREMENTS.md; the command overwrites the<TBD>placeholders when it runs. No automatic state to evolve.
Consequences for the AH dev:
- When you add a new phase to the inner cycle (or rename one), update
prompts/project-bootstrap.md's Phase E task-template substitution in lockstep — the bootstrap reuses the sametemplates/task.mdand the same R-NNNN linking logic as/ah:task-new. - When you add a new file to
.pi/codebase/(a hypothetical eighth thematic doc), decide whether it belongs in the intent-based set bootstrap writes from intent or in the observed-only set produced bymap-codebase. The four observed-only docs (STRUCTURE/INTEGRATIONS/TESTING/TECHNICAL_DEBT) are defined by the absence of meaningful intent before code exists. - If a dev runs
/ah:project-bootstrapon a project that's not greenfield, the pre-flight in step 1 stops the command. If a future use case asks for "rebootstrap" (reset and start over), don't weaken the pre-flight — add a separate/ah:project-rebootstrapcommand with explicit destructive semantics.
Starting from v0.22.0 every TASK.md frontmatter carries two optional fields — customer: and project: — both defaulting to null. They capture the commercial routing of a task: which customer (if any) is paying for the work, and which product / engagement the task is delivered against. The fields are pure metadata — no inner-cycle phase reads them, nothing changes in discuss / plan / execute / verify, and they are not part of the per-turn current-task-context injection. Their only consumer is /ah:project-status, which surfaces a [customer/project] tag next to each row so the dev can see at a glance which customer / product a task belongs to.
Why this exists: AH consumers fall along a spectrum — some teams build a single internal product, others run a customer-bespoke engagement, others sell the same product to N customers in parallel. The v0.21.x model had no way to mark "this task is for Acme on the Efesto deployment" vs "this task is generic core work". /ah:project-status listings looked identical regardless of commercial routing; the dev had to remember (or grep) which task served which customer. R-0017 closes that gap with the minimum possible surface area: two strings, optional, no behavior change. The shape (two separate keys rather than a single concatenated customer/project: "Acme/Efesto" string) keeps each value independently editable, queryable by grep, and unambiguous when only one of the two is known.
Decisions:
- Two fields, not one:
customer:andproject:are independent strings. Either, both, or neither may be set. Patterns the design covers:- Internal product sold to many: same
project:(e.g.Efesto) across many tasks,customer:varies per deployment (Acme,Globex,Initech). - Customer-bespoke engagement: same
customer:(e.g.Acme) withproject:denoting the engagement (Acme/HMI-2026). - Internal / uncommercial work: leave both
null. Rejected the single concatenatedcustomer/project:string because the convention (<customer>/<project>) is brittle (what if a customer name contains/?) and forces the dev to invent a placeholder for the missing half.
- Internal product sold to many: same
- Asked once, in
/ah:task-newstep 3-bis, optional: a single compact prompt accepting<customer> / <project>shorthand, withskip/ unparseable falling through tonull/null. No re-ask, no clarification turn — the dev edits the file by hand later if they change their mind. Priority and customer/project share the "set-and-forget at creation, edit by hand if it changes" posture; they are queue-management metadata, not work content. - Pure metadata, no behavior change: the inner cycle is deliberately untouched.
task-discuss/task-plan/task-verifydo not readcustomer:/project:and never inject them into the LLM context. Rationale: AH's per-turn context budget is already non-trivial, and the commercial routing of a task does not change the work itself — adding it to every turn would be pure token waste. The dev who needs the info can readTASK.mddirectly (or open thealt+p/alt+k/alt+cpopups, which already render the full frontmatter). - Rendered in every section of
/ah:project-status: the[customer/project]tag appears after(assignee, estimate)and before any phase tag, across In progress / In review / Backlog / Recently closed. Same value, same position, regardless of state — the commercial routing of a task does not change as it moves through the workflow, and a consistent column position makes scanning easy. When both fields arenull(the default for legacy tasks or for genuinely-internal work) the tag is omitted entirely — no empty[], no spacing artifact. This keeps the legacy display intact for consumers that never use the feature. - No new context injection, no new tool:
extensions/index.tsbefore_agent_starthandlers are not extended;load_codebase_docis unchanged; no helper module is added tolib/. The implementation surface is: template + frontmatter spec + one prompt step + one rendering rule + one migration. Matches the architectural posture of R-0007 (priority:). - Consumer migration
lib/migrations/v0_22_0.ts: walks every TASK.md in the four buckets and insertscustomer: null/project: nullright after theassignee:line where they're missing. Idempotent per key (insert only what's missing), non-destructive (never overrides an existing value). Matches the shape oflib/migrations/v0_10_0.ts(thepriorityretrofit). - Filename invariance: the keys are
customerandprojectin every locale. Body strings (the values) are author-supplied free-form and not localized by AH.
Consequences for the AH dev:
- When you add a new reader of TASK.md frontmatter, normalize the absence of
customer:/project:tonull(legacy files predating the v0.22.0 migration may still miss the keys until they reach a session that runs the migration). Match the priority pattern: treat missing as the documented default. - Do not extend
current-task-context(or any other context-injection handler) to shipcustomer:/project:. The token cost vs. value ratio was the deciding factor at design time — re-litigate that trade-off explicitly if a future use case appears, don't sneak it in. - If a future requirement asks for grouping / filtering
/ah:project-statusby customer or project (a "show me only Acme tasks" view), prefer a new flag on the command rather than retrofitting the field semantics. The two-field shape is meant to be queryable, not interactively filtered, at this stage. - A future migration that wants to rename / normalize existing
customer:/project:values must declare its own version. Don't extendv0_22_0.tsafter it ships — that file is append-only on missing keys by design.
- Distribution of third-party extensions other than AH.
- Publishing on the npm registry (changes the channel but not requirement R-0001 — can be a later iteration).
- Compliance with the Git Safety Rule (
CLAUDE.md): the extension does not mutate git state in the dev's repo. - Compatibility with the existing authoritative contracts (
WORKFLOW.md,task-layout.md). - The extension stays loaded by PI through the
default exportofextensions/index.ts.
- Does PI already offer primitives for
pi installand for reloading an extension? → Yes.pi install/pi updatedocumented indocs/packages.mdof PI v0.74.0 (npm, git, https, local paths). Reload viactx.reload()or session restart. - Does the OTA check run inside
index.tsor as a dedicated hook? → N/A: R-0002 declined in v0.5.0. PI has a nativePackage Updates Availablebanner that covers the use case without custom code. - Does the update proposal go through
pi.sendUserMessageor a dedicated UI channel? → N/A: R-0002 declined. No custom prompt: the user sees the PI banner and runspi updatemanually.
- v0.1.0 (PR #1): Distributable Pi Package (R-0001) + complete custom OTA (R-0002).
- v0.2.0 (PR #2): scope detection (project-local vs global) + pinning detection for OTA.
- v0.3.0 (PR #3): fix
pi update -l(the option does not exist in PI). - v0.4.0: test release to validate the OTA flow end-to-end (no code changes).
- v0.5.0: cleanup — custom OTA removed after observing PI's native banner. R-0001 unchanged.
- v0.6.0: test release to validate PI's native banner in a chain of consecutive releases (no code changes).
- v0.7.0: introduces R-0003 —
CHANGELOG.md(Keep a Changelog), GitHub Actionrelease.yml, consumer migration framework (lib/migrate-consumer.ts+lib/migrations/). Migration list still empty: the framework writes the marker but has nothing to apply yet. - v0.8.0: introduces R-0004 — peer-version compatibility check (
lib/check-pi-compat.ts) run atsession_startbeforemigrateConsumer. Warn loud non-blocking on three channels (console + UI toast + persistent message). Also introduces R-0005 — AH becomes English-only (prompts, skills, docs, console logs) and the consumer picks content language via.pi/ah-config.json. The first real consumer migration (lib/migrations/v0_8_0.ts) renames the 5 Italian-named codebase docs (INTEGRAZIONI → INTEGRATIONS, etc.), exercising the migration framework end-to-end. - v0.8.1: extends R-0005 — AH auto-creates
.pi/ah-config.jsonatsession_startwhen missing, with acontentLanguagechosen bydetectConsumerLanguage(Italian if existing.pi/codebase/*.mdcontent looks Italian — rescues legacy v0.7.x consumers; English otherwise). Makes the language choice explicit and committable instead of relying on a silent in-memory default. - v0.9.0: introduces R-0006 — consumer-side
.pi/REQUIREMENTS.md. Newtemplates/REQUIREMENTS.mdskeleton, consumer migrationlib/migrations/v0_9_0.tsthat drops an empty file on first session after upgrade, newimplements:frontmatter key onTASK.md, and integration into/ah:task-new(step 2-bis),/ah:task-discuss(step 3-bis + 7.5),/ah:task-plan(§3-bis read-only), and/ah:task-verify(Requirements DoD subsection)./ah:task-executeis deliberately unchanged. No new slash command — requirements grow organically. - v0.9.1: small UX tweak to
/ah:project-status— adds aRecently closedsection showing the last 5 tasks indone/sorted byupdateddesc. No new requirement. - v0.9.2: defensive fix to
lib/register-prompt.ts— prepends a short directive to any prompt body that references$EXT_DIRinstructing the agent that AH-internal paths arrive pre-resolved as absolute paths and must be read directly (nofind/locate/grep -r). Caps a long stall observed in/ah:task-newwhere some agents scanned the entire filesystem to "locate" the template. No new requirement. - v0.10.0: introduces R-0007 —
priorityfield onTASK.mdfrontmatter (LOW | NORMAL | HIGH | IMMEDIATE, defaultNORMAL)./ah:project-statusrenders the priority marker for every task and sorts the Backlog by priority desc. Consumer migrationlib/migrations/v0_10_0.tsretrofits the field asNORMALon every legacy task in the four task buckets. - v0.10.1: visibility fix for the priority badge — uses a fixed 4-char
[XY]token (!!/^/·/v) instead of a single-character column where NORMAL rendered as blank space. Limited to theIn progressandBacklogsections of/ah:project-status. - v0.11.0: drops the unused
/ah:standupslash command./ah:project-statuscovers the same use case. - v0.12.0: introduces R-0008 — three keyboard shortcuts (
alt+p,alt+k,alt+c) that open a TUI overlay listing the tasks of a single bucket. Newlib/show-task.tslisting/sort helpers andlib/task-popup.tsoverlay component. No consumer migration. - v0.13.0: introduces R-0009 —
/ah:helpslash command that opens a single-page overlay (lib/info-popup.ts) showing the AH version, the AH command list (discovered dynamically), and the shortcut list from R-0008. Registered viapi.registerCommandso it never burns an LLM turn. - v0.14.0: bordered popups (
lib/popup-frame.ts), reordered/ah:helpbody, newalt+hshortcut. No new requirement — purely visual / additive. - v0.14.1: emoji-aware width fix in
lib/popup-frame.ts(closes the right border of the box on lines containing wide BMP emoji like✅). - v0.15.0: introduces R-0010 (
alt+sbranch switcher + clean-tree gate) and R-0011 (help popup is the single source of truth for shortcuts). Newlib/branch-switch-popup.ts;TaskInfogrows abranchfield populated from frontmatter. - v0.15.1: fixes
alt+sruntime crash —execlives onpi(ExtensionAPI), not onctx(ExtensionContext). Three call sites inextensions/index.tsswitched fromctx.exectopi.exec. - v0.15.2: bumps
peerDependencies["@earendil-works/pi-coding-agent"]from^0.74.0to^0.75.0to silence the compat warning on PI 0.75.x. No API drift observed between 0.74 and 0.75 for the surfaces AH uses. - v0.16.0: introduces R-0012 — codebase-map logic moves from
prompts/map-codebase.md(auto-registered as/ah:map-codebase) toprocedures/map-codebase.md(inline sub-procedure referenced via$EXT_DIR). Drops the slash command. Four consumers updated to read the new path. No consumer migration. - v0.16.1: drops the obsolete
CODEMAP.md — deprecatedblock fromtask-layout.md. Single-line deprecation notice with no remaining callers anywhere in the repo — git history keeps the archeology. - v0.16.2: scrubs explicit consumer-project names ("Efesto") from
lib/context-inspector.ts,REQUIREMENTS.md, and earlierCHANGELOG.mdentries. AH is a generic Pi Package; source and docs must not name any one of the N possible consumers. - v0.17.0: introduces R-0013 —
lib/migrate-consumer.tsauto-commits.pi/ah-versionwhen the marker bump is the only thing dirty and the consumer is onmain/master. Removes the paper cut where everypi updateleft a stray modification at session start. - v0.17.1: untracks a stray
node_modules/typeboxsymlink (committed in4eba528against.gitignore, pointed at an absolute path on the original dev's machine — broken for everyone else).typeboxis already apeerDependencyprovided by PI. - v0.18.0: introduces R-0014 — ships first
LICENSE(PolyForm Noncommercial 1.0.0) and firstREADME.md.package.json#licenseswitches from the orphanMITdeclaration toPolyForm-Noncommercial-1.0.0. AH becomes source-available rather than orphan-MIT. - v0.19.0: introduces R-0015 —
/ah:project-statusderives the in-progresspctand phase fromPLAN.md+steps/NN-*.md(status:field), the same way for every in-progress task, dropping the v0.18.x split between "current task" and "others". Theprogress:frontmatter is no longer the source of truth — it stays as a last-resort fallback. New phase indicator[no plan]for tasks wheretask-startran but the inner cycle was bypassed. - v0.23.0: amends R-0007 —
/ah:task-newnow asks the dev forpriority:in step 3-ter (single compact prompt with the four canonical levels, case-insensitive,skipkeeps theNORMALdefault). Symmetric with the new R-0017 customer/project step in 3-bis. No schema change, no consumer migration: the template default and the four allowed values are unchanged. The dev can still ignore the question and editpriority:by hand later. - v0.22.0: introduces R-0017 — optional
customer:/project:fields onTASK.mdfrontmatter (both defaultnull)./ah:task-newstep 3-bis asks once (free-form shorthand<customer> / <project>, skippable)./ah:project-statusrenders a[customer/project]tag after(assignee, estimate)in every section; omitted entirely when both fields are null. Consumer migrationlib/migrations/v0_22_0.tsretrofits the two keys asnullon every legacy TASK.md, idempotent per key, non-destructive. Pure metadata: the inner cycle is unchanged and the per-turn context injection is not extended. - v0.20.0: introduces R-0016 —
/ah:project-bootstrap, the greenfield on-ramp. Single guided command that turns a fresh repo (no code) into a workable AH state: populated.pi/REQUIREMENTS.md(Context + first R-NNNN entries), three intent-based codebase docs (STACK.md/ARCHITECTURE.md/CONVENTIONS.mdwithsource: intentfrontmatter and<!-- intent:keep -->markers), and a first set of 3–7 backlog tasks each wired to the requirements. Defensive softening inskills/ah-task-discuss/INSTRUCTIONS.mdstep 2 (onlyARCHITECTURE+CONVENTIONSblocking;STRUCTUREadvisory) and inprocedures/map-codebase.md(preservesintent:keepblocks and augments under## Observed statewhen code arrives). No new consumer migration. - v0.23.2 / v0.23.3: declarative
peerDependencies["@earendil-works/pi-coding-agent"]bumps (^0.75 → ^0.76 → ^0.78) cut solely to silence the R-0004 compat warning on newer PI minors — the recurring no-op-release symptom that motivated removing the check entirely. - v0.24.0: decommissions R-0004 — deletes
lib/check-pi-compat.tsand itssession_starthook inextensions/index.ts. The compat check produced a false-positive[ah-pi-compat-warning]on every PI minor bump (PI's0.xscheme makes the^X.Y.Zcaret anchor on the minor), each silenceable only by a no-op AH release.peerDependenciesis retained as honest npm metadata but no longer gates startup. No consumer migration: nothing in the consumer changes.