AGENTS.md · git:20260924.8ccc98e · 2026-09-24 · sha256 3615b941e5ccff4c
AGENTS.md git:20260924.8ccc98eB
Immutable. This exact content never changes and is served at /api/v1/blob/3615b941e5ccff4c.
# dinostack
A portable package of the agentic engineering protocol for AI-assisted software development. It provides a structured delegation model, risk classification, adversarial review loops, code quality gates, git workflow conventions, and named agent definitions.
## Decisions
- Methodology source-of-truth: `content/sections/01-*.md`-`12-*.md` (12 sections; `06-capability-preflight.md` added by P2, renumbering 06-11 to 07-12). Edit `content/sections/` for any methodology change - `content/rules/agent-methodology.md` no longer exists.
- `scripts/build-methodology.sh` is the canonical assembly script; it also supports `--corpus minimal|medium|full` (DS-204, default `full` with no flag - byte-identical to the pre-DS-204 output). Only `.claude/` and `.gemini/` corpus-qualify their build: each invokes it TWICE per build - once plain/`--corpus full` for a full-corpus sibling file, once with `--corpus minimal --full-text-name <sibling-basename>` for the skill's trigger-loaded embed - so their skill bodies default to the minimal corpus while a full-corpus fallback stays reachable via a generated "Deferred at this corpus" pointer block. `.kimi/` and `.copilot/` (generating `.github/skills/`) were flipped the same way and then HELD BACK in the same DS-204 round (runtime QA came back INCONCLUSIVE for both - no drivable runtime on the verification host) - both invoke it plain, full corpus, unconditionally, same as `.codex/build.sh` (`bash "$REPO_DIR/scripts/build-methodology.sh"`, unaffected by DS-204) and every other adapter. `.cursor/build.sh` differs in output structure and skips it.
- `.claude/install.sh` manages `~/.claude/CLAUDE.md` via a `managed_content` Python string from `content/templates/claude-managed-content.md` (lean Skill Loading table). DS-143: the 3 @-import lines (METHODOLOGY.md, `code-standards.md`, `conventions.md`) drop when the skill symlink resolves (`SKILL_LINK_OK`) - methodology loads via `/dinostack` instead, embedded verbatim into `.claude/skills/dinostack/SKILL.md` by `.claude/build.sh`. Broken symlink: install.sh keeps imports and warns. Re-run install.sh after template changes. `module-manifest.md` (`content/rules/module-manifest.md`) is on-trigger only, excluded from the skill embed.
- `bootstrap.sh` (repo root, PR #109) is the public `curl | bash` installer: clones to `$(pwd)/DinoStack` (override `AE_DEST_DIR`), writes the resolved path to `~/.agentic/agentic-engineering-config.json` (`repo_dir`). `/ds-update-agentic-engineering` reads `repo_dir` at runtime (git-rev-parse validated, falls back to `~/DinoStack`).
- `skill_auto_load` opt-in enforcement (PR #97): `hooks/skill-auto-load-check.sh` wired per adapter - Claude/Codex: UserPromptSubmit; Gemini: BeforeAgent. Kimi's SessionStart nudge is an independent reimplementation, not a consumer of this file - a repo-wide grep for `skill-auto-load-check` returns no `.kimi` hit. OpenCode: `session.created` plugin (`.opencode/plugins/session-context.ts`). Cursor: no hook (loaded via .mdc). All install scripts use read-modify-write for config updates - bare overwrite destroys existing keys. OpenCode reads `~/.config/opencode/agentic-engineering.json`; all other adapters read `~/.claude/agentic-engineering.json`. DS-218 added an applicability filter: the hook fires when `skill_auto_load=true` and the turn's prompt looks like a software-development request, or when the prompt cannot be determined (fail-open) (see `hooks/AGENTS.md` for the fail-open contract).
- FE/QA methodology (P0+P1+P2) shipped 2026-05-28 in 14 PRs (#137-#140, #142-#145, #149-#154). `qa_criteria.method` enum is now 7 values: `browser`, `api`, `runtime-required`, `visual_conformance`, `accessibility`, `perceptual_diff`, `motion`. Capability preflight default is `blocking` as of P2 (9 agents populated; skeptic + orchestration-planner are deliberate no-ops). Canonical new reference docs: `content/references/capability-preflight.md` and `content/references/frontend-discipline.md` (7 sections + 8 Skeptic finding categories). Motion scenarios require Playwright (CDP `Emulation.setEmulatedMedia`); agent-browser cannot execute them. Storybook 6 ships URL format detection only; full SB6 framework adapter procedure improvements are deferred.
- Project-level identity override: `ds-identity --scope project` writes `<repo>/.agentic/identity.yml`. Effective identity resolves 4-tier confirmation-first: project-confirmed > global-confirmed > project-provisional > global-provisional > none. Gitignored per-developer. Resolver: `bin/ds-identity` + `hooks/stop-context.js` (5 regression tests in `bin/tests/test_agentic_identity.py`).
- Telemetry-commit-on-PR: `commit_telemetry` toggle in `.agentic/config.json` (default true) makes `/ds-implement-ticket` Phase 8 commit `.agentic/session-log/<dev>.jsonl` as a separate commit when identity is confirmed (`!.agentic/session-log/` carve-out in `.gitignore`). Path-aware PR-checkout resolution + HEAD-branch soft-fail guard prevents wrong-branch commits. Eventual-consistency: a session's own line lands in the NEXT ticket's Phase 8 commit; non-`/ds-implement-ticket` PRs are not covered.
- Progressive-disclosure kernel split (PR #340): the always-on methodology body keeps only the governance kernel inline; trigger-only detail lives in `content/references/**`, reached by read-on-trigger pointers. `content/references/design-goals.md` Goal 4 (three-question partition test) is canonical for inline-vs-defer. Sections 03/05/06/11 are NOT split - their reference files are supplements, not full copies.
- `.agentic/tasks.jsonl` ownership is defined by `bin/tests/fold_model.py` (DS-108, #521), not prose; ordering is arrival-order only. Gates: `test_fold_invariants.py`, `test_tasks_jsonl_fold.sh`.
- Host-harness instruction conflicts (#530, `d6b61e7b`; DS-132 generalized one collision to four): kernel rule in `content/sections/02-delegation.md`; catalog + delegation-suppression rule in `content/references/delegation-detail.md` §Harness-Injected Instruction Conflicts.
- No enforcement hook may deny conductor Read/Grep/Glob to force delegation, and none may gate on inferred session capability - flat prohibitions (#530): `hooks/AGENTS.md` §No gating on inferred session capability.
- `evals/` is git-untracked (decision #203), stays that way pending explicit re-tracking; invisible (not merely ignored) inside an isolation worktree. See `evals/AGENTS.md`.
- Post-merge local base-branch sync (#540, DS-121): `bin/ds-base-sync`, called unconditionally at `/ds-implement-ticket` Phase 12 - NOT nested under `auto_merge_on_ci_green` (defaults false). `diverged` requires BOTH `behind>0` AND `ahead>0`; ahead-only isn't divergence. Taxonomy/recovery: `content/references/base-branch-sync.md`.
- Hooks-snapshot dogfooding-gap closure (DS-54 follow-up): a merged hook fix stays dormant on this machine until an installer re-syncs the snapshot - the SessionStart nudge only fires at a brand-new session's first tool call, never mid-session or at merge time. Three mechanisms now close it: `bin/ds-doctor`'s `check_hooks_snapshot_staleness` check (on-demand; `--fix` calls `sync_hooks_snapshot`, the identical call every adapter `install.sh` already makes unconditionally; `half_applied` is NOT reported resolved by that sync alone - it only refreshes snapshot content, never an adapter's own hook config, so `--fix` on `half_applied` reports it unfixable with an actionable "re-run that adapter's install.sh" message; a nonzero exit from the classifier is a WARN, never a silent OK); `bin/ds-base-sync`'s non-blocking advisory note after every invocation (the one guaranteed post-merge trigger point, since Phase 12 calls it unconditionally; read-only, never mutates); and `bin/ds-update`'s `_hooks_snapshot_diverged` check, which forces the adapter-install loop on its `old_head==new_head` early-return path when the live hooks/ source hash diverges from the snapshot's stored hash - closing the case where an operator manually `git pull`ed a hooks change before running `ds-update`, leaving no diff for the normal rebuild-trigger logic to see (`scripts/update.js`, the interactive updater, has no direct equivalent on this path - it relies transitively on its own unconditional `runDoctor()` call instead). None of the three can auto-rewire a live session's hooks from a passive trigger - only an explicit `--fix` or an adapter `install.sh` run ever calls `sync_hooks_snapshot` (the DS-54 invariant). All three hashing call sites (the sync writer, the SessionStart classifier, and `ds-update`'s divergence check) call the SOLE `hooks_source_paths` function in `scripts/lib/hooks-snapshot.sh` - never a hand-copied path list. Friction cost: any operator with uncommitted local `hooks/` edits gets the full adapter-install loop forced on every `ds-update` "already up to date" run (the live hash differs from the last sync by construction) - expected, not a bug. Full detail: `hooks/AGENTS.md` §Downstream consumers "Merged is not live". Adapter asymmetry: `.cursor/install.sh` and `.opencode/install.sh` symlink hook config directly into the checkout instead of snapshotting it, so those two harnesses have no dormancy gap at all - Claude Code (the primary harness), Codex, Gemini, and Kimi are the ones subject to it.
- Resident-set budget ratchet (DS-68 redo, #532; redefined DS-143): `scripts/check-resident-budget.sh` measures only the post-manifest body of `content/templates/claude-managed-content.md` (the Skill Loading table - the sole content resident every session after DS-143 stopped `~/.claude/CLAUDE.md` `@`-importing METHODOLOGY.md/conventions.md/code-standards.md). Workflow `.github/workflows/resident-budget.yml`; suite `bin/tests/test_check_resident_budget.sh`. Required on `main` (ruleset `14778332`, 16 checks). THRESHOLD ratchets downward - lower it in the same PR as any deliberate shrink; run `bash scripts/check-resident-budget.sh` for the live figure, never trust a cited pair. Separate gate `scripts/check-skill-embed-budget.sh` (suite `bin/tests/test_check_skill_embed_budget.sh`) guards generated `.claude/skills/dinostack/SKILL.md` three ways: FLOOR (catches regression to pointer-only skill); CEILING (safety boundary on verified-safe injection size, not a tidiness budget - DS-45 found its own arithmetic is 1.1x a 2026-08-07 build-size snapshot (126,509 B, 598 B below the injection-confirmed 127,107 B figure), not 1.1x the injection-verified figure it claims to protect; the 2026-08-19 raise to 145,000 B was an explicit operator decision to unblock work, not a new measurement - see the CEILING constant's own comment); an embed-completeness check (DS-143 follow-up) pinning `EXPECTED_SECTION_COUNT`/`EXPECTED_RULES_COUNT` against `content/sections/[0-9][0-9]-*.md` and `content/rules/*.md` (excluding `module-manifest.md`) - bump the matching constant in the same commit as any source add/remove/renumber. `check-skill-embed-budget` is required on `main` too (count moved 15->16 when this job was promoted). **DS-45 sweep RUN 2026-09-03** via `scripts/skill-embed-sweep-harness.sh` + `docs/skill-embed-injection-sweep.md`: headless `claude -p` probes confirmed intact injection (tail canary, `declared_total_bytes`, and an exact `pad_block_sha256` match) at 140,000 / 145,000 / 150,000 / 160,000 B, so CEILING's current value (145,000 B) is now a genuinely verified-safe injection size - this result says nothing about whether raising CEILING is warranted; do not raise it on the basis of this sweep. Raising it remains a separate operator decision requiring its own new swept confirmation, exactly as before this sweep was run.
- Root `MEMORY.md` is untracked (DS-129), same rationale as `/decisions.md`/`.agentic/*`: methodology self-improvement scratch, not shippable. `MEMORY-archive.md` is deleted, the archive pattern retired (DS-130) - an archived entry stops being enforced once it leaves the always-loaded file. Fresh clones and worktree-isolated engineers have no `MEMORY.md`; put facts an engineer needs in `AGENTS.md` or the spawn brief.
- Command file size ratchet: `scripts/check-command-file-budget.sh` measures the raw on-disk byte size of `content/commands/ds-implement-ticket.md` directly (`wc -c`, no build step) - closing the gap left by `check-resident-budget.sh`, which deliberately measures only the assembled resident set and never sees command files at all. That file grew from 233,942 B to 363,761 B in about two weeks with zero shrink commits before this gate existed. Workflow `.github/workflows/command-file-budget.yml`, regression suite `bin/tests/test_check_command_file_budget.sh`. THRESHOLD_BYTES has been raised four times since gate creation (DS-188, DS-170 round 2, DS-170 round 3, DS-170 round 4) and is currently 395,000 B (~2% deliberate headroom over the 387,197 B live figure at the DS-170 round 4 raise - meant to fire on sustained growth, not the next paragraph). Shares `scripts/lib/budget-gate.sh` with the other budget gates (`grep -rln '^source .*lib/budget-gate.sh' scripts/*.sh` finds the current set), but not identically: every gate sourcing that library calls `budget_repo_dir`/`budget_file_bytes`, while only `check-resident-budget.sh` calls the exit-calling `budget_report` wrapper - this gate calls the underlying `budget_eval` directly instead (see the DS-182 paragraph below for why); `check-skill-embed-budget.sh` calls neither, hand-rolling its own OK-path tail (see the DS-182 paragraph below for why). **Advisory, not required**: it is deliberately NOT added to the `main` ruleset's required-checks list - promoting it to a blocking check is a separate operator decision - but the job itself is NOT `continue-on-error`: it fails (goes red, with a `::error::` annotation) on overage like any other check. THRESHOLD_BYTES ratchets downward over time - lower it in the same PR as any deliberate compression of the file; never raise it to accommodate un-triaged growth, and never trust a cited figure - run `bash scripts/check-command-file-budget.sh` for the live number.
- Command arg-substitution gate (DS-192): `scripts/check-command-arg-substitution.py` fails CI when a bare `$0`-`$9`/`$ARGUMENTS` token reappears inside an executable (bare/```bash/```sh/```shell/```zsh) fence anywhere under `content/commands/**` - the harness's slash-command literal-text argument substitution silently corrupts awk field-selector syntax at any such site before the model ever reads the instruction. Workflow `.github/workflows/command-arg-substitution.yml` (non-required, scan-only - no regenerate-then-diff step, since that would flip `content/commands/ds-implement-ticket.md`'s provenance under `scripts/gate-provenance.sh`'s D1 rule); regression suites `bin/tests/test_check_command_arg_substitution.sh` (the six named reddening mutations) and `bin/tests/test_arg_substitution_field_rewrites.sh` (old-form/new-form byte-identity on representative fixtures). Fixed 12 sites at ticket creation; each converted to whichever substitution-safe form fit its construct (dynamic-field awk, a pure-bash scan, or a non-`$N` comment wording).
- Per-PR delta axis on the two shared/merge-order-sensitive budget gates (DS-182): `budget_report`'s pre-DS-182 shape (prints OK/OVER BUDGET and `exit`s itself) is unchanged for its one remaining original caller, `check-resident-budget.sh` (byte-unchanged; it gained no git axis at all) - `scripts/lib/budget-gate.sh` split its body into a non-exiting `budget_eval` (returns 0/1) plus a thin `budget_report` wrapper (`if budget_eval "$@"; then exit 0; else exit 1; fi`). New library functions `budget_base_resolve <repo_dir> [base_branch]` (resolves `origin/<branch>` then `<branch>` against `-C <repo_dir>`, default `main`; returns 1 and prints nothing when git/a work tree/a resolvable ref is unavailable - takes `repo_dir` explicitly, same as `budget_delta`, so both always resolve against the caller's repo root rather than the process's own cwd), `budget_delta` (signed byte delta of a path between a base ref and the working tree; returns 2 and prints nothing when the path is absent at base - never conflated with a delta equal to the path's own full size), and `budget_burn_line` (one informational line, ALWAYS returns 0 and ALWAYS prints something - a distinct "burn: SKIPPED (...)" line, never a blank line, when git/a base ref/the base commit date is unavailable). Two different uses on the two gates that needed a git axis: `check-command-file-budget.sh` (hand-authored file - a PR's diff to it is that PR's own doing) gained a hard-failing per-PR `DELTA_LIMIT_BYTES` (32,936 B, re-derived as `ceil(max_observed_delta * 1.1)` from the 43 first-parent non-creation commits touching the file, `max_observed_delta = 29,941 B` at commit `d644217c`; fires 0/43 on the history it was derived from) - it calls `budget_eval` directly (not `budget_report`), since a delta breach is recorded (`delta_over=1`) rather than exiting immediately, so the `THRESHOLD_BYTES` `budget_eval` report still runs and prints even when both axes are over budget in the same run; the delta context is reported via `budget_eval`'s existing extra-context slot so `lines[-1]` of stdout (what `bin/ds-evaluate`'s `_collect_budget_gates` reads as the gate's summary) stays the `headroom:` line. `check-skill-embed-budget.sh` (a GENERATED artifact - its size on any branch reflects upstream `content/**` churn as much as that PR's own diff, the exact "merge order decides pass/fail" failure mode DS-182 exists to close, per KNW-20260818-001) gained only the informational `budget_burn_line` (a B/day burn RATE, `"burn: <B_per_day> B/day over <D> d - <N> d to limit"`, not a raw delta - `<D>` is the whole-day span since the resolved base ref's own commit date, floored at 1; `<N>` = headroom / burn rate, omitted when the rate is <= 0), no hard delta limit. Its OK-path tail is hand-rolled directly in the gate script rather than routed through `budget_eval`, because `bin/ds-evaluate`'s summary consumer depends on the exact pre-DS-182 wording (`ceiling:`/`headroom to ceiling:`), which `budget_eval`'s shared `threshold:`/`headroom:` labels cannot express; the burn line is computed once before the FLOOR/CEILING checks and printed on all three exit paths (OK, BELOW FLOOR, ABOVE CEILING), not only the OK path. Both workflow files needed `fetch-depth: 0` added to the relevant checkout step(s) (`command-file-budget.yml`'s single job; `resident-budget.yml`'s `check-skill-embed-budget` job only - `check-resident-budget`'s checkout is deliberately untouched, since that gate gained no git axis at all) or `origin/main` is unreachable on a default shallow checkout and the git axis silently degrades to its SKIPPED variant on every CI run. All three git-based functions degrade gracefully (never a hard failure) when git is absent or `repo_dir` isn't a git work tree - verified against every per-gate test suite (`grep -rl budget-gate.sh bin/tests/test_check_*.sh` finds the current set - `test_budget_gate_lib.sh` is a sibling library-level suite outside this set and does build a real `git init` fixture for its own DS-182 scenarios), which have always built their scratch fixtures with zero `git init` at all. Deciding which axis a NEW budget-gate target needs (a hard per-PR `DELTA_LIMIT_BYTES` for an AUTHORED/hand-edited target, vs. an informational `budget_burn_line` for a DERIVED/generated one) is not a fresh judgment call each time: run `bash scripts/gate-provenance.sh <repo-relative-path>` first - it classifies the target as `AUTHORED` or `DERIVED` against the real CI workflows and manifest headers and cites which rule fired. DS-183/184/185/186 (any future budget-gate axis work) should run it before assigning an axis, not re-derive the classification by hand.
- Dependency-advisory gate: `scripts/check-npm-audit.sh` audits BOTH lockfiles (`package-lock.json`, `scripts/package-lock.json`) and fails only on advisories with a **non-breaking fix available**. The predicate is actionability, NOT severity, and NOT `fixAvailable` truthiness - both of those red-line `main` today (measured: `npm audit` exits 1 on `scripts/`, and all 4 of its high-severity advisories - on 4 distinct packages (`extract-zip`, `@puppeteer/browsers`, `puppeteer-core`, `@marp-team/marp-cli`), all rooted in `extract-zip` - report `fixAvailable` as a truthy object proposing a `@marp-team/marp-cli@2.4.0` downgrade flagged `isSemVerMajor: true`; `extract-zip`'s newest published version is 2.0.1 and its advisories cover `*`, so the package is genuinely unpatchable). The discriminator is `isSemVerMajor !== true`; a breaking-only fix is an operator call, reported on every run but never a failure. `npm audit` resolves from the lockfile with NO `node_modules` present, so the gate needs no install step (~0.5 s per manifest). Job: `npm-audit` in `.github/workflows/codeql.yml` (placed there for the weekly `schedule` - the failure class is drift, not diff; see `.github/workflows/AGENTS.md`). Regression suite `bin/tests/test_check_npm_audit.sh` drives the predicate through `--classify` with fixtures, since live output changes whenever the upstream advisory DB does. **Advisory, not required** - deliberately off the `main` ruleset's required-checks list, same precedent as `check-command-file-budget` and `check-slide-overflow`, but the job is NOT `continue-on-error` and goes red on an actionable advisory. Covered by `scripts/check-local.sh` (whose preflight requires `npm`); it is the only covered gate needing NETWORK, which no preflight can require, so it splits on `${CI}` - loud SKIPPED locally, hard red in CI. It can never go green in CI having asserted nothing, but a green LOCAL `check-local.sh` run is not proof the audit ran; read the gate's own output. Precedence when manifests disagree: an actionable advisory DETECTED in one manifest outranks another manifest failing to audit, and is reported as exit 1 (naming the unaudited manifest too) rather than discarded in favour of a skip. **Retirement is keyed to an observable, not to `.github/dependabot.yml`** - that file already configures npm updates for both directories, and security alerts are governed separately from version-update scope. The test: next time the gate goes red, check whether an update PR proposing that same fix was already open BEFORE it fired. **That test is only valid on an advisory whose remediating package is NOT covered by an `ignore:` rule**, because an `ignore` rule suppresses security updates as well as version updates. `.github/dependabot.yml` ignores `@marp-team/*` on `/scripts`, and every advisory `scripts/` carries today remediates through `@marp-team/marp-cli`, so for that population the test is guaranteed to answer "no PR was open" by deliberate config choice rather than by the automation failing - answering the retirement question on it would keep the gate alive on evidence that could never have come out any other way. If the gate has only ever gone red on ignored packages, the test has not been run yet.
- CI trigger policy (2026-09-03): workflows are PR-only by default; the post-merge run is CONSOLIDATED, not deleted. Do NOT re-add `push: branches: [main]` to an individual workflow, and do NOT delete `.github/workflows/verify-merged-tree.yml` on the theory that strict required checks make it redundant - that theory is false here. Ruleset `14778332` sets `strict_required_status_checks_policy: true` BUT also carries `bypass_actors: [{RepositoryRole admin, bypass_mode: always}]`, and `gh pr merge --admin --squash` is the normal unattended-agent merge path, so a behind-base PR can merge without being brought up to date. Measured over the 25 most recent first-parent merges by comparing each squash commit's tree object to its PR head commit's tree object: 19 matched, 6 did NOT (#850, #854, #855, #856, #857, #860 - 24%; #850 differed across 46 files). `verify-merged-tree.yml` (push:main + workflow_dispatch) runs a ~10s `needs-full-run` gate comparing those two trees and skipping the suite when they match. Its fail-open behavior is TWO-AXIS: inside the script, AND at the job boundary, where all 10 suite jobs guard on `if: ${{ !cancelled() && (needs.needs-full-run.result != 'success' || needs.needs-full-run.outputs.run_full == 'true') }}` - a bare `if: needs.<job>.outputs.<x> == 'true'` gets GitHub's implicit `success()` and would SKIP the suite when the gate FAILS, inverting the contract exactly when the gate is least trustworthy. Never simplify that expression: the `${{ }}` wrapper is mandatory (a bare leading `!` opens a YAML tag and is a scanner error), and `!cancelled()` is preferred over `always()` so an operator cancel does not start all 10 jobs. Two runner-semantics traps that bit this workflow and generalize: (1) GitHub runs a `run:` block as `bash -e {0}` unless the step sets `shell:`, so error-fallthrough branches written on a "no errexit" assumption are DEAD - the gate step sets `shell: bash {0}` deliberately, and any harness testing a `run:` block must read the step's own `shell:` key rather than assuming `bash -c`; (2) `git rev-parse <bad-ref>` prints its argument to STDOUT and exits 128, so an emptiness test on its output never fires - use `--verify`. `push: main` is NOT only merges: `changelog-publish.yml` pushes `CHANGELOG.md` + `docs/changelog.html` daily with a PAT (`CHANGELOG_PUSH_TOKEN` - a PAT DOES trigger workflows) and no PR, so the gate has a second `run_full=false` path for no-PR pushes whose `<sha>^ <sha>` diff is a subset of a two-entry `UNTESTED_SAFE_PATHS` allowlist of files no check reads; keep it tiny, it retires when changelog-publish opens a PR instead. The cheap way to keep `run_full=false` the norm for merges is `gh pr update-branch --rebase` before an `--admin` merge (works as of local gh 2.100). A separate 0-catches-in-600-runs/31-merges measurement is evidence that ~19 duplicate runs per merge was the wrong shape, NOT that the post-merge run is unnecessary. `codeql.yml`, `gitleaks.yml`, `scorecard.yml` keep `push: main`. `bin-tests.yml` still has its own push trigger: when that is dropped, its three jobs MUST be added to `verify-merged-tree.yml` in the same change. Canonical policy: `.github/workflows/AGENTS.md`.
## Tools
- GitHub operations: use `gh` CLI, not GitHub MCP
- `gh pr create` requires an authenticated `gh` session (`gh auth status`) - run `gh auth login` if needed.
- `rm -rf` is blocked by Claude Code permissions here; remove files individually: `rm <file>` then `rmdir <dir>`
- `bin/ds-memory` - lightweight retrieval tool for querying `.agentic/events.jsonl`, `MEMORY.md`, `.agentic/context.md` on demand.
- `bin/ds-base-sync` - fast-forwards the local base branch after a merge without a checkout; the only way to move a local branch ref outside an explicit checkout+pull.
- Force-push (`--force`, `-f`, `--force-with-lease`) is blanket-denied by the harness permission layer for subagents AND the conductor; chat authorization doesn't clear it. Push to a NEW branch name instead of rebasing a branch with an open PR.
- `bin/ds-tracker` - manages the project-local gitignored `.agentic/tracker.yml` overlay (`init`/`set`/`show`/`path`/`resolve`). Applied after the four-step `AGENTS.md` tracker chain: overlay is sole source (`_source: "overlay"`) when `AGENTS.md` declares no tracker or a different one; merges field-by-field (`_source: "merged"`) only when both declare the same tracker. Refuses git-tracked paths; `--force-unignored` overrides.
- The Graphify PyPI package is genuinely named `graphifyy` (double-y); CLI/skill command stay `graphify` - verified against upstream README, do not "fix" it.
- `git checkout <sha> -- <path>` inside a worktree resurrects files a later commit deleted (adds files present at `<sha>`, doesn't replay deletions). When basing a step on a non-`main` commit this way, `git rm` anything it deleted and gate on an exact `git ls-files` set.
## Deploy
- The docs site (docs.dinostack.ai) auto-deploys on push to `main` via Vercel git integration (~3 min); verify with `curl -sI https://docs.dinostack.ai/<path> | grep last-modified`. There is no manual deploy step; do NOT run `vercel --prod` from a local CLI unless its authenticated scope demonstrably contains the `dinostack-docs` project (a mis-linked deploy once caused a live 404).
- `dinostack-docs` Vercel project's Root Directory is `docs/`, so Vercel reads `docs/vercel.json` - repo-root `vercel.json` is ignored for redirects/rewrites/headers (once caused a live install-URL redirect to silently 404). Put Vercel redirect/rewrite/header changes in `docs/vercel.json`.
## Conventions
- **Base branch:** `main` is the sole integration branch for this repo. Declaration: `BASE_BRANCH: main`.
- **Workflow for all implementation work (non-Trivial risk):**
1. `git fetch origin` to ensure latest `main`.
2. Spawn subagent Workers using `isolation: "worktree"` with worktrees branched from `origin/main`. Worktree path: `.agentic/worktrees/<branch-name>`.
3. Worker implements, runs quality gates (lint, typecheck, tests), commits.
4. Push branch to origin: `git push -u origin <branch-name>`.
5. Open PR against `main` via `gh pr create`.
6. Once CI/CD checks pass, auto-merge: `gh pr merge --squash --delete-branch`.
7. Clean up: isolation worktrees are removed after push; feature worktrees after merge. Session-start prune + branch-prune remain as backstops. See `content/references/worktree-lifecycle.md` §Isolation worktree cleanup commands and §Feature worktree cleanup commands.
8. Update local main: `bin/ds-base-sync <this-checkout-path> main` (run from this repo's own checkout, since it's both ticket repo and tool source) - fast-forwards local `main` without a checkout. Fall back to `git checkout main && git pull --ff-only origin main` only when `bin/ds-base-sync` is not present at that path.
- Steps 6-8 are automatic - never pause for merge approval when CI is green.
- `main` enforces strict required status checks: a PR whose base has moved must be rebased and re-run before merge - GitHub does not re-run workflows on base movement. Sync with `gh pr update-branch --rebase`; the default (merge) update produces an unsigned commit that fails the DCO check.
- Failed CI is a hard stop - investigate before proceeding.
- **Conductor never edits shippable artifacts directly, even Trivial one-liners** - delegate to a worktree-isolated `engineer` from `origin/main`; conductor may only edit exempt artifacts (`.agentic/`, conductor-direct prints/decisions/resolver execution). Classifier: `content/rules/conventions.md` §Git Workflow.
- Superseding an open PR: close + rebase, never bundle (`content/rules/conventions.md` §Git Workflow).
- Sequential isolation:worktree spawns: the worktree is cleaned up between them, so later Workers fall back to the main tree - tell them explicitly. Round-N rework mechanic: `content/rules/conventions.md` §Git Workflow, `content/references/worktree-lifecycle.md` §Round-N rework mechanic. (DS-123)
- Parallel engineer spawn briefs must say "branch your worktree from current `origin/main`, NOT a local checkout that may include not-yet-merged sibling units" - worktree branch leaks are the most common cross-unit contamination class.
- Multi-unit PR assembly: `main` requires DCO Signed-off-by on every commit. Conductor `git merge --no-ff` is unsigned - cherry-pick each engineer's signed commit (no merge commits) or `git merge --signoff`. Never force-push to fix; `gh pr merge --admin --squash` discards merge commits anyway.
- Pre-commit auto-stages `.codex/agents/*.toml` only in the primary checkout with a staged `content/` change (same as `.claude/skills/dinostack/{METHODOLOGY,SKILL}.md`) - inside a linked worktree the hook exits early before any build or staging runs. Since an agent is always in a worktree, stage it manually after any `content/` edit, or extend `git add` in `hooks/pre-commit`.
- A green `bin/tests/` run is not the gate set - the required `hooks-js-tests` check pins methodology prose byte-for-byte and lives outside `bin/tests/`, so it is entirely possible to have a green `pytest bin/tests/` and a red PR. There is now a single literal command for the locally-runnable part of the set: `bash scripts/check-local.sh` (exit 0 every covered gate passed, 1 a gate failed, 2 toolchain preflight failed - see CONTRIBUTING.md "Local toolchain"). Its coverage is deliberately partial and it says so: three gates are excluded on cost (the full Codex skill suite, the slides build plus overflow checks, and the gitleaks full-history scan), and every run ends with a `NOT RUN LOCALLY (run in CI)` block naming them and their exact commands. A green run means the covered set passed, never that CI will be green - read that block. The hooks JS set is derived, never hand-counted: `list_hooks_js_tests` in `scripts/lib/ci-test-discovery.sh` transcribes the `hooks-js-tests` job's own loop (glob `hooks/tests/test-*.js` minus the `case`-arm quarantine into the separate `wrap-lock-tests` job), pinned against a fresh inline re-derivation by `bin/tests/test_ci_test_discovery.sh`. That exclusion is not a local exemption: `check-local.sh` runs the two excluded files as its own `wrap-lock-tests` gate, and `bin/tests/test_check_local.sh` breaks each in turn to prove it. Do not infer membership from a filename - `test-wrap-lock-abandoned.js`, `test-wrap-lock-session-id.js`, and `test-wrap-lock-verdict.js` all run in `hooks-js-tests`, not `wrap-lock-tests`. Two of those prose-pinning tests are `test-wrap-md-partA-parity.js` (asserts `/ds-wrap` retains its whole-flow lock) and `test-wrap-context-format-golden.js` (sha256-pins `content/references/wrap-context-format.md` from its NORMATIVE marker to EOF).
- **Docs-currency pass is mandatory before any work is declared complete and before its PR merges** - ticket work or ad-hoc, regardless of which paths changed. Enumerate the surfaces the diff touched, then verify and update any related documentation that now states something stale; land doc updates in the SAME PR as the work. Candidate doc surfaces to check: `docs/index.html`, `docs/slides/*.md` (plus rebuilt `.html` via `bash scripts/build-slides.sh`), `README.md`, `CONTRIBUTING.md`, `content/SKILL.md`, and the relevant `AGENTS.md` sections. This generalizes the `content/`-triggered bullet below: that bullet's trigger stays mandatory for `content/` edits specifically, while this pass applies to all work. Skeptic tiering per `content/references/doc-sync-obligation.md`.
- Any change to `content/rules/`, `content/references/`, `content/agents/`, `content/commands/`, or `content/sections/` MUST also update `docs/index.html` and any affected `docs/slides/*.md` (then run `bash scripts/build-slides.sh` and commit the regenerated `.html` - do not hand-edit the `.html`; upgrading marp is an intentional same-PR action: bump `scripts/package.json` + regenerate `scripts/package-lock.json` + rebuild) in the same PR; additionally, any now-stale count, list, or reference in `README.md`, `CONTRIBUTING.md`, or `content/SKILL.md` is in-scope for the same PR. Public-facing intent debt is the worst kind. The canonical trigger predicate and tiered Skeptic classification for this obligation live in `content/references/doc-sync-obligation.md`; Skeptic reports missing docs sync as Minor (`Blocking-minor:` when a public-facing doc actively misleads). Grep the decks by hand: `check-slides-sync` only compares each `.md` against a fresh render, so an assertion that is stale in both the `.md` and its `.html` passes the gate.
- Any new interactive prompt in `.claude/install.sh` MUST use `ae_confirm()` (reads `/dev/tty` when available, defaults "no"). Never bare `read -p` - it aborts under `set -euo pipefail` + piped stdin (the `curl | bash` path).
- After any `content/SKILL.md` change, run `.pi/build.sh` and `.claude/build.sh` to regenerate adapter SKILL.md copies. Pre-commit does not auto-stage these.
- Mutation-testing a `content/` spec assertion requires `bash scripts/build-all.sh` BEFORE running the test - mutating `content/` alone trips the byte-identity assertion and falsely confirms a semantic assertion caught the change.
- `codex-skill-sync` CI validates exactly 4 generated native skills under `.codex/skills/`. After changing a canonical input: `python3 scripts/codex-skills.py inventory --repo . > .codex/skill-compatibility.yml`, then `.codex/build.sh` - never hand-edit generated skill files. That redirect truncates the file to 0 bytes before Python runs, so an aborted run (e.g. a `SkillError`) leaves it empty on disk - `git checkout -- .codex/skill-compatibility.yml` restores it. Prefer a temp-file + `mv` on success, or check exit status first.
- `REPO_DIR` is distinct from `REPO`: `REPO` is the ticket's own project checkout, `REPO_DIR` is the dinostack install where `bin/`-tools live. Resolve via `scripts/lib/repo-dir.sh` (`resolve_repo_dir`): `repo_dir` from `~/.agentic/agentic-engineering-config.json` when valid, else `~/DinoStack`. `bin/ds-resolve-worktree` and `bin/ds-base-sync` both depend on it.
- Any local adapter-drift check must copy its pathspec verbatim from `Fail on adapter drift` in `.github/workflows/adapter-sync.yml` - 11 adapter dirs PLUS `.github/copilot-instructions.md`, `.github/agents`, `.github/prompts`, `.github/instructions`, `.github/hooks`, `.github/skills`. `scripts/build-all.sh`'s `ADAPTERS` array omits those six; `git diff --exit-code` silently exits 0 on a nonexistent pathspec. (`.github/skills` added DS-186.) That pathspec now has TWO in-repo consumers, `adapter-sync.yml` and `verify-merged-tree.yml`; the duplication is deliberate (extracting it into a composite action would move the assertion out of a workflow file and stop `gate-provenance.sh`'s D1 rule classifying the adapter dirs as DERIVED) and is kept honest by a step in the latter's `merged-adapter-sync` job asserting the two copies are byte-identical.
- Verify DCO author/sign-off with lowercase `git log --format=%ae` (raw) - uppercase `%aE` and `git show` both APPLY `.mailmap`, which can show a mismatched author email on a correctly signed commit.
- Build commits with porcelain `git commit -s`, never `git commit-tree` - the plumbing command takes no `-s` (fails DCO) and skips pre-commit hooks entirely.
- A new module isn't done until something in `content/` invokes it - shipping uncalled and passing review is the default failure. Assert wiring explicitly, as `check_prose_wiring()` in `bin/tests/test_worktree_lifecycle_spec.sh` does.
- `docs/planning/` is gitignored - a worktree-isolated engineer branched from `origin/main` cannot read a plan there. Restate any obligation in the spawn brief; never keep the same binding prose in both (a fix to one copy drifts silently).
- No CI gate checks for em dashes, so scope the check to your own diff, never the whole file: `git diff --cached -U0 | grep '^+.*—'` (staged) or `git diff -U0 origin/main...HEAD | grep '^+.*—'` (branch). A whole-file grep is useless here - 13 em dashes pre-exist on `main` in the resident files (`content/rules/code-standards.md` 5, `content/sections/04-risk-classification.md` 8) and will match on every run. That count is the baseline: leave those alone, and treat any added line that matches as a defect to fix before committing - except the two in this bullet's own grep patterns, which are the literal search target and must not be "fixed" (doing so silently breaks both commands).
- A shell gate whose assertions are guarded by `command -v <tool>` must hard-fail under `${CI}` rather than skip, or the job goes green asserting nothing. `bin/tests/test_check_resident_budget.sh:155-156` is the pattern; `.github/workflows/bin-tests.yml:57-58` installs zsh so the guard never fires in CI. A silently-skipped assertion is indistinguishable from a pass in the job log.
- Editing the `main` ruleset requires the admin `gh` account - a non-admin token's read of ruleset `14778332` can return `bypass_actors` empty when the admin read returns an actor. Treat any unfetched-as-admin `bypass_actors` as unverified, not empty - a PUT built from it can wipe the entry. Back up the full ruleset JSON before any PUT and diff field-by-field after.
- Session knowledge belongs in root `MEMORY.md` - untracked by design (DS-129), sole-copy risk accepted; a worktree-isolated engineer branched from `origin/main` cannot see it, and neither can a fresh clone. Never end a session holding uncommitted `MEMORY.md` edits with content that exists nowhere else - confirm sole-copy status with `git log --all -S "<phrase>"` plus `git stash list` (`--all` does not cover `refs/stash`) before letting the session end. Tracked `AGENTS.md` files must never accumulate uncommitted session-end edits; any change to them is a normal reviewed PR like any other shippable file, never a session-end commit obligation.
- An `isolation:"worktree"` engineer spawn with a named branch can leak that branch into the conductor's own checkout and switch its HEAD (once corrupted `main`). After every worktree-isolated spawn, verify the conductor's own checkout is still on its expected branch (`git branch --show-current` + `git status` + `git diff --stat`) before committing anything from it.
- Isolation worktrees always branch from `main` HEAD, never from a named feature branch. A continuation fix for an existing feature branch can't be briefed as "branch from `origin/<feature>`" - it silently starts from main instead; cherry-pick or push the commit directly onto the existing branch's remote tip instead.
- After an isolation-worktree engineer commits, the main checkout's view of that branch ref can lag behind the worktree's own HEAD. Push the explicit commit SHA (`git push origin <sha>:refs/heads/<branch>`), not the branch name, or `gh pr create` can reject a stale-base push.
- An adapter-rebuild/baseline-regen commit (or one built from a stale local snapshot) can silently revert unrelated files with CI green - once reverted 51 files from three merged PRs; PR #390/#432 (DS-47) similarly reverted unrelated list items/counts/cross-refs. Verify a "regenerate generated artifacts" commit's diff touches *only* the intended paths; diff a PR's base..tip for unrelated hunks before merge/rebase; spot-check a key new symbol lands on `main` after a multi-PR feature, not just "merged."
- `.claude/install.sh` has a zsh variable conflict: a local variable named `status` is read-only in zsh. Avoid `status` as a variable name in zsh-specific code; run affected blocks in a bash subshell for mixed-shell compatibility.
- Empty-array expansion under `set -u`: use `${ARR[@]+"${ARR[@]}"}` with the outer expansion unquoted. Figures are argc, measured 2026-09-24 by placing the expansion between two literal arguments and having the callee report `$#`, written as unset/empty/two-element for /bin/bash 3.2.57 | bash 5.3.9 | zsh 5.9, so 2 means the expansion added nothing and 3 means it added one empty word. Safe form `${ARR[@]+"${ARR[@]}"}`: 2/2/4 | 2/2/4 | 2/2/4, and a spaced element stays one word. `"${ARR[@]:-}"`: 3/3/4 | 3/3/4 | 3/3/4. `"${ARR[@]-}"`: 3/3/4 | 3/3/4 | 3/2/4. Either `-` form can therefore add an empty positional where you wanted nothing, breaking any consumer that rejects one: `bin/ds-cleanup-worktrees` exits 2. Bare `"${ARR[@]}"`: abort/abort/4 | 2/2/4 | abort/2/4. Outer-quoting the safe form is wrong under zsh: `"${ARR[@]+"${ARR[@]}"}"` is 2/2/4 | 2/2/4 | 3/3/4. A `[[ ${#ARR[@]} -gt 0 ]]` pre-guard is equally sound and is what most sites here use. Pin such a construct by executing the shipped block against an argc stub (`check_step2_age_passthrough_argv`, `bin/tests/test_worktree_lifecycle_spec.sh`), never by grepping its spelling. DS-189 recorded the wrong remedy in untracked root `MEMORY.md`, never in this file; DS-252 corrected it.
- When `origin/main` advances mid-session, a diff against the new `origin/main` can make an unrelated, already-merged PR's files look reverted - a diff-base artifact, not a real one. Diagnose by comparing your commit's parent to `origin/main`; fix by rebasing onto current main and re-running any generator the new commits may have changed.
- Checking for duplicate in-flight work only at spawn time misses collisions starting (or merging) mid-run - two live sessions once independently fixed the same three tickets. Check open PRs and the shared checkout's `git status` before spawning, and re-check for a superseding merge right before opening a PR (a dirty `mergeStateStatus` on a fresh PR is the after-the-fact symptom). When superseded, stand down and cite the superseding PR rather than racing a rebase.
- Four git lessons: (1) flip a gitignored file to tracked via an additive negation (`!.agentic/<file>`), never remove the ignore rule; (2) a negation cannot re-include a file whose parent **directory** is excluded (`.agentic/`, `/.agentic/`, bare `.agentic` defeat `!.agentic/qa.md`) - only glob forms (`/.agentic/*`, `**/.agentic/**`) work; (3) matching is **last-match-wins** - a negation only overrides an umbrella **above** it (`bin/agentic-migrate` once appended umbrellas at EOF and killed every carve-out, fixed #527); (4) `git diff --quiet <ref> -- <path>` exits 0 for a path never committed to `<ref>` ("unchanged", not "new") - pair with `git cat-file -e <ref>:<path>` (fixed #534). Green CI per-PR doesn't guarantee `main`'s end state under concurrent sessions - assert with `git ls-files` / `git check-ignore`.
- Merging to `main` needs exactly 1 code-owner approval, not 3 (verified against the live ruleset: `required_approving_review_count: 1`; CODEOWNERS designates `* @tysonhummel` as sole owner). `--admin` is still required for `gh pr merge` when the PR is authored by @tysonhummel (GitHub disallows self-approval), or no `@tysonhummel` reviewer is present - the normal unattended-agent case. Confirmed merging #483-#486; an approval-required failure on `--admin` means the bypass actor was removed from the ruleset and needs re-adding in GitHub's UI.
- The live Claude Code harness auto-locks every `isolation:"worktree"` worktree at creation. Directory: `.claude/worktrees/agent-<agentId>` (NOT `worktree-agent-<id>` - that's the branch name); branch: `worktree-agent-<agentId>`; `git worktree list --porcelain` shows `locked claude agent agent-<agentId> (pid <pid> ...)`. Non-force `git worktree remove` refuses it (`remove -f -f` needed) independent of any methodology-level lock. Match cleanup detection on the branch-name pattern, not the directory-path substring.
- Work in an isolation worktree can silently absolutize the `.claude/skills/dinostack/{agents,commands,references,rules}` symlinks to that worktree's path - the symlink still resolves on the breaking machine, so `check-adapter-sync` misses it and a merged absolutized symlink breaks the skill for every other clone. `scripts/check-symlinks-relative.sh` (`adapter-sync.yml`) gates this in CI; before staging, check `git status --short` for those four paths, restore with `git restore --staged --worktree`; audit a landed commit with `git ls-tree -r <sha> .claude/skills/dinostack | awk '$1=="120000"{print $4}'` then `git cat-file -p <sha>:<path>` (leading `/` = broken). Recurs on every build inside an isolation worktree - re-check before every stage. (DS-96, DS-104)
- Moving inline prose into `content/references/*-detail.md` creates two silent hazards, neither producing a conflict marker: (1) the original site drifts after extraction as later commits keep patching it while the copy diverges; (2) the extracted block can arrive already wrong, since a verbatim move preserves whatever it asserted and reviewers read the move as a no-op (DS-68's move carried a stale byte figure; PR #420). Re-verify every count/path/figure against live state in the same PR - "verbatim" is a diff property, not a correctness one.
- A verification step piped through BSD `xargs -I{}` can assert nothing and still pass: real paths exceed `-I`'s ~255-byte budget, so `xargs` returns rc=1 and emits nothing, leaving the comparator comparing empty to empty. `2>/dev/null` on a comparison step compounds this into a false pass.
- Adapter/methodology build discipline: `content/` is single-sourced into all adapter dirs; any change requires `bash scripts/build-all.sh` (11 dirs) then `git diff --exit-code -- <adapter dirs>`. `content/sections/**` changes also need `scripts/.methodology-baseline.sha256` regenerated (`bash scripts/check-methodology-drift.sh --regenerate`). `check-adapter-sync` tests the merge result, not your branch - rebase onto `origin/main` and rebuild first.
- Repo checkout convention: `DinoStack`; Most adapters use skill name and directory `dinostack` (`.claude/skills/dinostack/`, etc.). Codex uses `dinostack-codex` plus `dinostack-codex-{brief,wrap,implement-ticket}` in the shared `~/.agents/skills/` namespace to avoid overriding Gemini payloads; its installer migrates only exact-owned old-name symlinks. Config FILENAMES/activation markers did NOT rename: `~/.claude/agentic-engineering.json`, `~/.agentic/agentic-engineering-config.json`, `agentic-engineering: opt-in`/`opt-out`/`-profile:`/`-preset:` markers (live in consumer repos we don't control). Slash commands use `/ds-*`; `bin/agentic-*` CLI names unaffected. (DS-26)
- Some `content/` source files are hardlinked (not copied) into their adapter destinations, and this is inconsistent across files - check the mechanism, not a fixed list, which goes stale: grep the target `build.sh` for `hardlink_from_content(`, a bare `ln ` (no `-s`), or `os.link(` - any adapter build whose script calls one of these against a `content/` source genuinely hardlinks it (same inode, mutates in place on a `content/` edit) rather than copying it. A hand-typed snapshot of which scripts/counts this resolves to has repeatedly gone stale here (DS-90 Unit 5 review) - do not restore one. `bash scripts/build-all.sh` is still required after any `content/` edit regardless of hardlink status, since a fresh git worktree checkout breaks all hardlink relationships (git does not track hardlinks - every worktree gets independent inodes) until rebuilt. (DS-78, DS-90)
- Adapter dirs contain hand-authored sources `build.sh` does NOT regenerate (`.codex`/`.copilot`/`.cursor`/`.gemini`/`.github` hook files, `.opencode/plugins/session-context.ts`) - check manually after any adapter source edit or cross-adapter pattern change. (DS-26)
- Bulk sweeps over `evals/` must exclude git worktrees (`evals/.worktrees/wt-*` are pinned at historical commits) to avoid dirtying checkouts. (DS-26)
- On a GLM (or any non-Claude-backed) session, spawning a methodology subagent fails immediately - every agent's `model:` frontmatter is a Claude alias and the Agent tool's `model` enum is Claude-only. Don't attempt Worker/Skeptic delegation; go conductor-direct with mechanical verification, or switch to a Claude model first.
- Never include `"mode": "opt-in"` in doc or adapter example JSON unless demonstrating opt-in activation - it silently disables the methodology on repos without an `agentic-engineering: opt-in` marker. Show only the field being demonstrated (e.g. `{ "profile": "relaxed" }`) or use `"mode": "opt-out"`.
- Marp dark-theme reskin gotchas (theme leaks, table/code-syntax fixes): see `docs/slides/AGENTS.md`.
- Slide overflow gate (DS-203): `scripts/check-slide-overflow.sh` (`check-slide-overflow` job in `slides-sync.yml`) is wired advisory-only - deliberately NOT on the `main` ruleset's required-checks list, matching the command-file-budget gate's precedent, but the job itself is a real red job on overage, not `continue-on-error`. `scripts/slide-overflow-baseline.json` is membership-only (deck basename -> positional section ids, no px values) - after any edit that inserts, removes, or reorders slides in a debt-carrying deck, re-run `node scripts/check-slide-overflow.js --dump-overflows`, diff, and commit the updated baseline in the same PR; never hand-typed. The live figure is not hand-typed here either - run `jq '[.[] | length] | add' scripts/slide-overflow-baseline.json` for the current pre-existing overflow count. The sole load-bearing determinism mechanism is a fresh headless-Chrome browser context per deck (a reused page converges on wrong values, measured). Known residual: a hypothetical future deck relying only on system fonts (no `@font-face` at all) would trip the font fail-closed predicate's "no font face reached loaded status" axis even though nothing is actually broken - update the predicate if that ever happens.
- A shell check written as `cmd 2>/dev/null | head -N || echo 'no records'` can never report its negative case: `||` binds to `head`, which exits 0 on empty input, so the fallback is unreachable. Same vacuous-pass family as the `command -v` guard and the em-dash check. Test the branch you expect to fire by making it fire on purpose.
- When a methodology sentence is found to be false, prefer DELETION over a narrowed rewrite, and check first whether anything is load-bearing on it. A narrowed rewrite reads as the careful option but re-asserts the same shape of claim against the same unverified surface: DS-150's replacement for "**Subagents do not have hooks.**" was itself false. Both original sentences were load-bearing for nothing and deletion was the correct fix. Corollary: a claim removed from `content/` is not gone until its `docs/slides/*.md` copy is grepped by hand - `check-slides-sync` compares each deck against a fresh render and passes when the `.md` and `.html` are stale in agreement.
- **Read `docs/overview/vision.md` before designing any change to this repo's own methodology surface** (`content/**`, `hooks/**`, `bin/**`, `scripts/**`, adapter dirs) and name the pillars that cut against the proposal, not only the ones that support it. Nothing enforces this mechanically: no hook, no CI gate, and `check-vision-alignment` inspects PR-body prose only and never opens the file. Several agent contracts and commands require the same read within their own scope - that is not a reason to skip it here.