From b6c14b4cb9eba17057bd49596dd30ec7a9ed4b52 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:16:22 -0400 Subject: [PATCH 01/19] docs: remove every em dash from tracked text The audit counted 1,809 em dashes in 128 files at the 2026-10-18 baseline; the port releases rewrote most of them and 139 remained in 16 files (no en dashes). Each is rewritten with a colon, comma, semicolon, period or parentheses so the sentence still reads. Code changes are limited to comments and printed strings: the handoff banner, two selftest failure messages, the sync-check header and separator, and the ops-verify unit description. No test greps any of the changed strings. --- .../bin/claude-window-ping-selftest.sh | 8 +- .claude-config/bin/grabit | 6 +- .claude-config/bin/secrets-scan.sh | 6 +- .claude-config/hooks/handoff-check.sh | 12 +- .claude-config/hooks/session-work-selftest.sh | 10 +- .claude-config/systemd/ops-verify.service | 2 +- .claude-handoffs/key.sh | 4 +- .gitattributes | 2 +- CONTEXT/project-kata.md | 108 +++++++++--------- CONTEXT/team.md | 10 +- CONTEXT/tools.md | 10 +- CONTEXT/work-tracking.md | 54 ++++----- DELIVERABLES/README.md | 4 +- NOTES/MASTER/README.md | 4 +- PROJECTS/sync-check.sh | 16 +-- SKILLS/design-method/brand.md | 14 +-- 16 files changed, 135 insertions(+), 135 deletions(-) diff --git a/.claude-config/bin/claude-window-ping-selftest.sh b/.claude-config/bin/claude-window-ping-selftest.sh index 6ca52bb..f5aa99b 100755 --- a/.claude-config/bin/claude-window-ping-selftest.sh +++ b/.claude-config/bin/claude-window-ping-selftest.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# claude-window-ping-selftest.sh — prove the window-ping pipe handles every +# claude-window-ping-selftest.sh: prove the window-ping pipe handles every # path it can hit unattended: good profiles, config typos, missing binary, # nonzero/timeout rcs, empty config, overlap locking, and tmp hygiene. Runs # entirely against a MOCK claude binary and a scratch state dir (the -# CC_WINDOW_PING_BIN / CC_WINDOW_PING_STATE_DIR test hooks) — no real API +# CC_WINDOW_PING_BIN / CC_WINDOW_PING_STATE_DIR test hooks), no real API # usage, no touch of the real ~/.local/state/window-ping. # # Exit 0 = every case behaves; 1 = defect. @@ -71,7 +71,7 @@ CC_WINDOW_PING_MODEL="custommodel" CC_WINDOW_PING_PROFILES="m=$WORK/cfg-m" bash chk "CC_WINDOW_PING_MODEL passed through" "$(grep -q -- "--model custommodel" "$MOCK_RECORD"; echo $?)" # 6. Missing binary: one rc=127 row PER configured profile, complete atomic file. -# PATH mimics cron's minimal /usr/bin:/bin — coreutils work, claude is absent. +# PATH mimics cron's minimal /usr/bin:/bin (coreutils work, claude is absent). reset CC_WINDOW_PING_BIN="$WORK/does-not-exist" PATH="/usr/bin:/bin" CC_WINDOW_PING_PROFILES="a=$WORK/x b=$WORK/y" bash "$SCRIPT" chk "missing binary -> 2 rc=127 rows" "$([ "$(awk -F'\t' '$2=="127"' "$STATE/last-status.tsv" | wc -l)" = 2 ]; echo $?)" @@ -108,6 +108,6 @@ if [ "$fails" -eq 0 ]; then echo "claude-window-ping-selftest: PIPE BEHAVES" exit 0 else - echo "claude-window-ping-selftest: $fails case(s) failing — do not trust the cron pipe" + echo "claude-window-ping-selftest: $fails case(s) failing: do not trust the cron pipe" exit 1 fi diff --git a/.claude-config/bin/grabit b/.claude-config/bin/grabit index 5aed0b8..270c0df 100755 --- a/.claude-config/bin/grabit +++ b/.claude-config/bin/grabit @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# grabit — move files between this (often headless) OPS box and the machine +# grabit: move files between this (often headless) OPS box and the machine # you're driving it from, over Tailscale. No SFTP, no cloud; tailnet-encrypted, # never public (uses `tailscale serve`, not `funnel`). # @@ -16,7 +16,7 @@ # grabit -h | --help # # One-time per box (already done on this one): sudo tailscale set --operator=$USER -# so transfers need no sudo. Only ever stages files you name — never exposes the +# so transfers need no sudo. Only ever stages files you name and never exposes the # directory they live in, so a neighbouring .env can't leak. set -euo pipefail @@ -73,7 +73,7 @@ serve(){ rm -rf "$SERVE_DIR"; mkdir -p "$SERVE_DIR" local p; for p in "$@"; do [ -e "$p" ] || die "no such path: $p"; cp -r "$p" "$SERVE_DIR/"; done # Serving a path is privileged even for an operator; needs (passwordless) sudo. - # Taildrop (the default push) does NOT need this — prefer it where sudo is absent. + # Taildrop (the default push) does NOT need this, so prefer it where sudo is absent. sudo tailscale serve --bg "$SERVE_DIR" >/dev/null 2>&1 \ || die "tailscale serve failed (needs sudo + tailnet HTTPS enabled; just use a plain 'grabit FILE' push instead)" local url; url="https://$(self_dns)/" diff --git a/.claude-config/bin/secrets-scan.sh b/.claude-config/bin/secrets-scan.sh index 62c722a..3612a69 100755 --- a/.claude-config/bin/secrets-scan.sh +++ b/.claude-config/bin/secrets-scan.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# secrets-scan.sh — literal-credential detector for memory/lessons surfaces. +# secrets-scan.sh: literal-credential detector for memory/lessons surfaces. # # Usage: # secrets-scan.sh FILE|DIR [...] scan files/dirs (md files), print hits @@ -12,8 +12,8 @@ # ", "1Password entry X"), never literal values. This scanner backs # secrets-guard.sh (write-time), verify-ops.sh (repo gate), and any # pre-push scan. Two pattern tiers: -# HARD — unambiguous credential shapes; always flagged. -# SOFT — assignment-style (password:, api_key=); skipped when the line +# HARD: unambiguous credential shapes; always flagged. +# SOFT: assignment-style (password:, api_key=); skipped when the line # already reads as a pointer (sops/vault/1password/.env/redacted), # because pointers are exactly what we WANT written. set -uo pipefail diff --git a/.claude-config/hooks/handoff-check.sh b/.claude-config/hooks/handoff-check.sh index 081227c..a050f08 100755 --- a/.claude-config/hooks/handoff-check.sh +++ b/.claude-config/hooks/handoff-check.sh @@ -1,15 +1,15 @@ #!/usr/bin/env bash -# handoff-check.sh — SessionStart notifier for PROJECT-KEYED handoff batons. +# handoff-check.sh: SessionStart notifier for PROJECT-KEYED handoff batons. # # Surfaces a PENDING baton for THE CURRENT PROJECT ONLY. Batons live at # ~/OPS/.claude-handoffs/pending/.md (key = git repo root or # cwd; see key.sh). Because the banner is keyed to the session's own project, -# parallel sessions on different projects NEVER see each other's batons — +# parallel sessions on different projects NEVER see each other's batons: # this is the fix for "handoffs picking up another agent's work". # # Notify ONLY. Never auto-loads context (that would re-inject stale state # every session). The session-handoff skill does the actual pickup and -# archives the baton so this stops firing. Exit 0 always — a surface, not a +# archives the baton so this stops firing. Exit 0 always, a surface, not a # gate. If you run more than one profile via CLAUDE_CONFIG_DIR, this fires in # each of them (settings.json is symlinked into every config dir). # @@ -22,7 +22,7 @@ set -uo pipefail HANDOFF_DIR="${OPS_DIR:-$HOME/OPS}/.claude-handoffs" KEY_SH="$HANDOFF_DIR/key.sh" -# Current project key — shared logic via key.sh, with an inline fallback so a +# Current project key: shared logic via key.sh, with an inline fallback so a # missing/non-exec helper degrades gracefully instead of crashing the hook. if [ -x "$KEY_SH" ]; then KEY="$(bash "$KEY_SH" 2>/dev/null)" @@ -35,7 +35,7 @@ fi BATON="$HANDOFF_DIR/pending/$KEY.md" # Legacy single-baton fallback (pre project-keying): surface ONLY if its -# recorded cwd matches this session — never blast an old global baton. +# recorded cwd matches this session, never blast an old global baton. if [ ! -f "$BATON" ] && [ -f "$HANDOFF_DIR/ACTIVE_HANDOFF.md" ]; then _legacy="$HANDOFF_DIR/ACTIVE_HANDOFF.md" _lcwd="$(grep -iE '^cwd:' "$_legacy" 2>/dev/null | head -1 | sed -E 's/^[^:]*:[[:space:]]*//')" @@ -62,7 +62,7 @@ cur="default" cat </dev/null 2>&1; then pass "sta # 2b. REGRESSION GUARD: resume with no stamp (revived session) -> MUST write rm -rf "$T/.claude-compact-cycle" printf '{"source":"resume"}' | runinit -if ls "$T"/.claude-compact-cycle/session-start-* >/dev/null 2>&1; then pass "revive (source=resume, no stamp) writes — revive-never-stamps guard"; else fail "revive did NOT stamp — the revive-never-stamps bug is back"; fi +if ls "$T"/.claude-compact-cycle/session-start-* >/dev/null 2>&1; then pass "revive (source=resume, no stamp) writes: revive-never-stamps guard"; else fail "revive did NOT stamp: the revive-never-stamps bug is back"; fi # 3. existing fresh stamp preserved across a re-fire (write-if-absent) rm -rf "$T/.claude-compact-cycle" @@ -46,7 +46,7 @@ f="$(ls "$T"/.claude-compact-cycle/session-start-* 2>/dev/null | head -1)" if [ -n "$f" ]; then echo "SENTINEL=keep" >> "$f" # a rewrite (> "$STAMP") would wipe this printf '{"source":"compact"}' | runinit - if grep -q '^SENTINEL=keep' "$f" 2>/dev/null; then pass "existing fresh stamp preserved on re-fire (t0 intact)"; else fail "existing stamp was REWRITTEN — t0 clobbered on re-fire"; fi + if grep -q '^SENTINEL=keep' "$f" 2>/dev/null; then pass "existing fresh stamp preserved on re-fire (t0 intact)"; else fail "existing stamp was REWRITTEN: t0 clobbered on re-fire"; fi else fail "could not create a stamp to test preservation" fi diff --git a/.claude-config/systemd/ops-verify.service b/.claude-config/systemd/ops-verify.service index 4ff58fb..c357cae 100644 --- a/.claude-config/systemd/ops-verify.service +++ b/.claude-config/systemd/ops-verify.service @@ -1,5 +1,5 @@ [Unit] -Description=OPS drift gate (verify-ops.sh) — nightly report for session-briefing +Description=OPS drift gate (verify-ops.sh): nightly report for session-briefing [Service] Type=oneshot diff --git a/.claude-handoffs/key.sh b/.claude-handoffs/key.sh index de38fcc..6220f88 100755 --- a/.claude-handoffs/key.sh +++ b/.claude-handoffs/key.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# key.sh — the single source of truth for the handoff PROJECT KEY. +# key.sh: the single source of truth for the handoff PROJECT KEY. # # Emits a stable, filesystem-safe key for the current project so the WRITE # (session-handoff skill), the READ (same skill), and the notifier @@ -8,7 +8,7 @@ # Key = git repo toplevel if inside a repo, else the current working dir; # slashes → dashes, leading dashes stripped. handoff-check.sh inlines this # same logic as a fallback so a missing/non-exec helper degrades gracefully -# rather than crashing the hook — keep the two in sync if you change it. +# rather than crashing the hook; keep the two in sync if you change it. set -uo pipefail diff --git a/.gitattributes b/.gitattributes index 1894896..76e5934 100644 --- a/.gitattributes +++ b/.gitattributes @@ -18,7 +18,7 @@ *.bat text eol=crlf *.cmd text eol=crlf -# Binary — never diff or normalize +# Binary: never diff or normalize *.png binary *.jpg binary *.jpeg binary diff --git a/CONTEXT/project-kata.md b/CONTEXT/project-kata.md index 8c3007b..210872c 100644 --- a/CONTEXT/project-kata.md +++ b/CONTEXT/project-kata.md @@ -1,6 +1,6 @@ # Project Kata -> A portable system for organizing any repository — documentation, +> A portable system for organizing any repository: documentation, > scaffolding, and the rules that hold them together. Not tied to a > particular project, language, or domain. Apply wherever the doc set > is starting to sprawl, the root directory is growing files faster @@ -24,14 +24,14 @@ that follow from them. 1. **The root holds canonical entry points only.** A repo's root has four documentation files, no more: - - `README.md` — _what exists_. The first file a visitor reads. - - `CONTRIBUTING.md` (or your project's equivalent — `CLAUDE.md`, - `AGENTS.md`, `STYLE.md`, etc.) — _how we work in this repo_. The + - `README.md`: _what exists_. The first file a visitor reads. + - `CONTRIBUTING.md` (or your project's equivalent: `CLAUDE.md`, + `AGENTS.md`, `STYLE.md`, etc.): _how we work in this repo_. The first file a contributor reads. - One spec / rulebook file (commonly `RULEBOOK.md`, `SPEC.md`, - `RFC.md`, or `ARCHITECTURE.md`) — _the rules of the road_. The + `RFC.md`, or `ARCHITECTURE.md`): _the rules of the road_. The authoritative document everything else defers to. - - `CHANGELOG.md` — _every merged change_. The history. + - `CHANGELOG.md`: _every merged change_. The history. Any new file at the root needs a positive justification, not just "couldn't think of where else to put it." If you want to add a @@ -58,8 +58,8 @@ that follow from them. **User-facing help is the same principle, applied externally.** If the project has a UI and non-technical users, user-facing docs - live where users find them — in-app, a `/docs` route, a customer - portal — not in the repo root. Repo docs are for contributors; + live where users find them (in-app, a `/docs` route, a customer + portal), not in the repo root. Repo docs are for contributors; user docs are for users. Don't mix them. 4. **No fact lives in two places.** If you find yourself writing the @@ -80,7 +80,7 @@ that follow from them. 6. **Soft cap on file lengths.** README around 500 lines. The spec/rulebook is the deliberate exception (it's a spec). Any other file approaching the cap is a signal to split it. A long file - isn't comprehensive — it's a sign the topic isn't carved up well + isn't comprehensive; it's a sign the topic isn't carved up well yet. That's the whole kata. The rest of this file is application detail. @@ -116,14 +116,14 @@ groups entries: | Backlog | Unscoped + scoped work | `IDEAS.md`, `ROADMAP.md` | Migrations get their own subdirectory because they're write-once, -historical-after — they shouldn't crowd the active runbooks. +historical-after; they shouldn't crowd the active runbooks. --- ## The README ↔ CONTRIBUTING boundary The two most-read files in any repo are the README and the -how-we-work file (CONTRIBUTING.md, CLAUDE.md, AGENTS.md — whatever +how-we-work file (CONTRIBUTING.md, CLAUDE.md, AGENTS.md, whatever the project calls it). They have distinct jobs and content drifts between them constantly if the boundary isn't enforced. @@ -170,9 +170,9 @@ Every file in `docs/` follows the same pattern: ## See also -- `` — what it covers -- `` — what it covers -- `` §
— the underlying rule +- ``: what it covers +- ``: what it covers +- `` §
: the underlying rule ``` The opening blockquote and the closing "See also" are @@ -197,7 +197,7 @@ The procedure when something needs to be written down: - Adjacent to code? Yes for component-specific notes. 3. **Add a row to `docs/README.md`** with a one-line "what it - covers". Don't skip this — an unindexed doc is invisible. + covers". Don't skip this: an unindexed doc is invisible. 4. **Add a "see also" footer** to the new file pointing at the relevant entry points (the rulebook section it derives from, any @@ -222,10 +222,10 @@ discipline can fix later. When creating a new project, ask before scaffolding. Use structured multiple-choice questions, not open-ended ones: -1. **Initialize a git repo?** — `Yes (recommended)` / `No` -2. **Project type?** — `Work` / `Personal` (drives license + copyright defaults — see overlays) -3. **Public or private?** — `Public` / `Private` -4. **License?** (Work, or Personal+Public) — `MIT (default)` / `Apache 2.0` / `None` +1. **Initialize a git repo?** `Yes (recommended)` / `No` +2. **Project type?** `Work` / `Personal` (drives license + copyright defaults; see overlays) +3. **Public or private?** `Public` / `Private` +4. **License?** (Work, or Personal+Public) `MIT (default)` / `Apache 2.0` / `None` - Personal + Private: skip license entirely. Don't scaffold past these questions. If any answer is unclear, ask @@ -234,10 +234,10 @@ before creating files. ### Required files at the root Every git-initialized project starts with these files. They are -exempt from Rule 1 (canonical entry points) — they're not +exempt from Rule 1 (canonical entry points): they're not documentation, they're tooling configuration. -**`.gitattributes`** — prevents line-ending and binary-diff issues +**`.gitattributes`**: prevents line-ending and binary-diff issues between Windows and Linux contributors: ``` @@ -261,7 +261,7 @@ between Windows and Linux contributors: *.bat text eol=crlf *.cmd text eol=crlf -# Binary — never diff or normalize +# Binary: never diff or normalize *.png binary *.jpg binary *.jpeg binary @@ -276,7 +276,7 @@ between Windows and Linux contributors: *.eot binary ``` -**`.gitignore`** — baseline patterns. Extend with +**`.gitignore`**: baseline patterns. Extend with language/framework-specific entries based on the stack: ``` @@ -322,7 +322,7 @@ workflows. If the project uses other agent tooling (`.cursor/`, `.aider/`, etc.), add those too. None of these should ever be committed. -**`README.md`** — at minimum, project name and a one-line description. +**`README.md`**: at minimum, project name and a one-line description. ### Public-repo additions @@ -338,19 +338,19 @@ the title: ``` **`LICENSE`** file based on the user's choice. Copyright line -depends on project type — see overlays. +depends on project type; see overlays. -**`.env.example`** if the project uses environment variables — +**`.env.example`** if the project uses environment variables: document every required variable with placeholder values, never real credentials. -**Tighter `.gitignore` review** — confirm coverage of all secret +**Tighter `.gitignore` review**: confirm coverage of all secret patterns, build outputs, and environment-specific files before the first push. ### Private-repo notes -- `.gitignore` still covers secrets — private isn't an excuse for +- `.gitignore` still covers secrets: private isn't an excuse for laziness here. Repos get cloned, forked, and re-shared. - No `LICENSE` needed unless explicitly requested. - README doesn't need the sensitivity banner, but should still note @@ -368,7 +368,7 @@ follow this kata, the files are: | The rules / spec | `ARCHITECTURE.md` | System design, decision records | | Every merged change | `CHANGELOG.md` | Keep a Changelog format | -`ARCHITECTURE.md` is the spec/rulebook slot — required only for +`ARCHITECTURE.md` is the spec/rulebook slot, required only for mid-to-large projects. Break it out of `README.md` when the README crosses ~500 lines (the kata's soft cap) or when three or more major sections start competing for space (infra maps, data flow, @@ -378,8 +378,8 @@ service dependencies, onboarding). Secrets never live in the repo. The pattern: -- A `SECRETS.md` pointer file is allowed at the root or in `docs/` - — it lists *where* credentials actually live (1Password, Key +- A `SECRETS.md` pointer file is allowed at the root or in `docs/`: + it lists *where* credentials actually live (1Password, Key Vault, vendor-specific stores). It contains zero secrets itself. - Pair with a pre-commit hook that blocks `.env` files and common credential patterns from reaching staging. @@ -389,16 +389,16 @@ Secrets never live in the repo. The pattern: ### Image-publishing repos: GHCR retention workflow Any repo that publishes container images to GHCR gets a retention -workflow at scaffold time — registries accumulate versions forever, +workflow at scaffold time, because registries accumulate versions forever, and the cost surfaces only after thousands of stale versions pile up (one dashboard repo hit ~3,000 before its first cleanup). The standard shape is `ghcr-cleanup.yml` using `dataaxiom/ghcr-cleanup-action`, manual-first: -- `workflow_dispatch` only — no schedule until the config has +- `workflow_dispatch` only: no schedule until the config has proven itself on a real run. -- A `dry_run` input defaulting to `true` — nothing deletes until +- A `dry_run` input defaulting to `true`: nothing deletes until someone deliberately runs it dry, reviews, then runs it live. - Keep the last N tagged versions (10-25 depending on release cadence). @@ -413,7 +413,7 @@ Two platform gotchas, learned live: lands on the default. Check this before declaring the workflow deployed. - **Guessed package names 404.** The workflow's package list must - name packages that actually exist in the registry — a plausible + name packages that actually exist in the registry: a plausible but never-published name fails the run. Always dry-run first and confirm the package exists and `latest`/pins are excluded before the real deletion. @@ -423,7 +423,7 @@ Two platform gotchas, learned live: Per Rule 1, backlog files (`IDEAS.md`, `ROADMAP.md`, `BACKLOG.md`) are not canonical entry points. They live in `docs/`, not at the root. A backlog at the root is the most common -form of root sprawl in practice — resist it. +form of root sprawl in practice, so resist it. Keep the backlog one flat file (`docs/IDEAS.md`), not a `docs/ideas/` subfolder. Folders of improvement files become @@ -445,7 +445,7 @@ non-canonical doc is taking it away from the four that matter. ### Duplicated paragraphs Writing the same thing in two places "for convenience." Convenient -to write, expensive to maintain — the second copy goes stale within +to write, expensive to maintain: the second copy goes stale within a quarter. Always link by anchor. ### Topic creep @@ -491,7 +491,7 @@ The kata is a default, not a law. Break it when: - **The rule conflicts with a tool's expectations.** Some tools expect specific filenames at the root (`LICENSE`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `.github/`). Those files get an automatic - exception — they're not documentation in the kata's sense. + exception, since they're not documentation in the kata's sense. - **You're at the very start.** A brand-new repo with two files in total doesn't need a `docs/` directory. The kata kicks in when the doc set is starting to sprawl, not before. @@ -579,7 +579,7 @@ Four files to read. Everything else is on demand. ## Sandboxed-agent git constraint -**Scope: sandboxed agent environments only** — any agent surface +**Scope: sandboxed agent environments only**: any agent surface that runs shell commands inside a sandbox against a host-mounted filesystem (Anthropic's desktop app is the common case). Claude Code, chat Claude, and any other environment have no such @@ -587,7 +587,7 @@ constraint and handle git normally. Don't carry this rule outside the sandbox. **Inside the sandbox, 99% of repo work is fine.** Edit files, read -files, restructure folders, run scripts, browse history — all +files, restructure folders, run scripts, browse history. All normal. A sandboxed session is a perfectly capable environment for working in a repo. @@ -600,11 +600,11 @@ subsequent git operations on the host. The rule: - Read-only git commands (`status`, `log`, `diff`, `show`, - `branch -l`) — fine in the sandbox. -- File edits, scaffolding, doc work, code changes — fine in the + `branch -l`): fine in the sandbox. +- File edits, scaffolding, doc work, code changes: fine in the sandbox. The lock-file issue is about the `git` CLI specifically, not about touching files in a repo. -- Git write commands — don't run them from the sandbox shell. +- Git write commands: don't run them from the sandbox shell. Either let the Operator run them outside the sandbox, or switch to Claude Code if a continuous build-commit-iterate cycle is needed. @@ -616,7 +616,7 @@ has a single home. ## Operator overlays -Everything above this section is portable — apply it to any repo, +Everything above this section is portable: apply it to any repo, any team, any domain. This section is where the kata's portable rules meet one specific setup: project portfolio layout, LICENSE defaults, which file plays which canonical role, backlog @@ -624,20 +624,20 @@ conventions, and anything else true of *your* machine and *your* organizations but not true of the kata in general. This section ships as a template. `BOOTSTRAP.md` fills it in on -first run — it asks a short set of structured questions (repo home +first run: it asks a short set of structured questions (repo home path, organization name(s), license defaults, which file plays the spec/rulebook role) and writes the answers here as a living record. Until bootstrap has run, treat the entries below as illustrative -only — they are not real configuration. +only; they are not real configuration. -### EXAMPLE — project portfolio + license defaults +### EXAMPLE: project portfolio + license defaults *(Synthetic. Replace this entire subsection with your own answers -during bootstrap — it shows the shape an overlay entry takes, not a +during bootstrap; it shows the shape an overlay entry takes, not a default to inherit.)* Project repos live under `PROJECTS//`. The -organization folder is mandatory — never drop a repo directly under +organization folder is mandatory: never drop a repo directly under `PROJECTS/`. Example organizations for a two-context setup: `ExampleCorp` (work), `personal` (side projects). New organizations get added when warranted; the canonical description of current @@ -647,13 +647,13 @@ clusters, cross-repo relationships, and keyword routing lives in | Project type | License default | Copyright line | | ------------ | --------------------- | ---------------------------- | | Work | MIT | Copyright (c) Example Corp | -| Personal | None (skip the file) | n/a — only add if asked | +| Personal | None (skip the file) | n/a (only add if asked) | If a personal project is going public and a license is wanted, ask -before defaulting — don't auto-attach a work organization's name to +before defaulting, and don't auto-attach a work organization's name to a personal repo. -### EXAMPLE — layered IDEAS files +### EXAMPLE: layered IDEAS files *(Synthetic, same caveat as above.)* @@ -671,9 +671,9 @@ one: Each file declares its scope at the top and routes anything narrower down to the next level. IDEAS and TODO are interchangeable -in this layout — pick one and stick with it per file. CHANGELOGs +in this layout: pick one and stick with it per file. CHANGELOGs are not duplicated at cluster level (a cluster folder is a -grouping, not a project — track its changes via the parent harness +grouping, not a project; track its changes via the parent harness CHANGELOG). Add further overlay entries below these the same way, as your diff --git a/CONTEXT/team.md b/CONTEXT/team.md index dd10fba..a06a25c 100644 --- a/CONTEXT/team.md +++ b/CONTEXT/team.md @@ -1,6 +1,6 @@ # team (context slot) -> **This is a template — it has not been filled in yet.** Fill it in during +> **This is a template; it has not been filled in yet.** Fill it in during > `BOOTSTRAP.md`, or the first time a skill needs to know who owns what. Delete > this banner once the file is real. A blank team slot is a valid state: every > skill that reads it degrades gracefully (see the degrade below). @@ -11,11 +11,11 @@ address a person by role reads this instead of hard-coding names. Fill in, per person or role: -- **Name / handle** — how they appear in tools and in the operator's speech. -- **Role** — what they own and decide (e.g. approver for X, owner of repo Y). -- **Reach** — how the operator refers to them ("my partner", "the lead"), so a +- **Name / handle**: how they appear in tools and in the operator's speech. +- **Role**: what they own and decide (e.g. approver for X, owner of repo Y). +- **Reach**: how the operator refers to them ("my partner", "the lead"), so a loose mention resolves to the right person. **Degrade when absent:** if no roster is configured, address roles generically -and use no names — "the approver", "whoever owns that repo" — and ask the +and use no names ("the approver", "whoever owns that repo"), and ask the operator when a specific person actually matters. diff --git a/CONTEXT/tools.md b/CONTEXT/tools.md index 048cad8..3fad8eb 100644 --- a/CONTEXT/tools.md +++ b/CONTEXT/tools.md @@ -1,6 +1,6 @@ # tools (context slot) -> **This is a template — it has not been filled in yet.** Fill it in during +> **This is a template; it has not been filled in yet.** Fill it in during > `BOOTSTRAP.md`, or the first time a skill needs to know which integrations > exist on this host. Delete this banner once the file is real. A blank tools > slot is a valid state: every skill that reads it degrades gracefully (see the @@ -12,12 +12,12 @@ rather than one it assumes. Fill in, per integration: -- **Namespace** — the MCP server prefix as it appears to the AI (e.g. +- **Namespace**: the MCP server prefix as it appears to the AI (e.g. `myserver__`), plus the one-line "what it is". -- **What it covers** — the systems behind it (ticketing, RMM, docs, cloud, ...). -- **Any fixed identity** — a resource id, tenant, or default the tools always +- **What it covers**: the systems behind it (ticketing, RMM, docs, cloud, ...). +- **Any fixed identity**: a resource id, tenant, or default the tools always need, if one applies. **Degrade when absent:** if no tool map is configured, use only the tools the -session actually exposes right now — never assume a namespace exists — and ask +session actually exposes right now (never assume a namespace exists), and ask the operator when a task needs an integration that isn't visible. diff --git a/CONTEXT/work-tracking.md b/CONTEXT/work-tracking.md index 80cb3a5..0458320 100644 --- a/CONTEXT/work-tracking.md +++ b/CONTEXT/work-tracking.md @@ -1,9 +1,9 @@ # Work Tracking -> **This is a template — it has not been filled in yet.** +> **This is a template; it has not been filled in yet.** > Until you fill it in, this file describes *what a work-tracking config holds*, > not *your systems*. Every block below marked **EXAMPLE** is synthetic (Example -> Corp flavor). The examples exist to show the shape each section needs — they +> Corp flavor). The examples exist to show the shape each section needs. They > are not defaults to inherit and they are not your real tools. > > **Fill this in during `BOOTSTRAP.md` (or the first time you close a session).** @@ -11,48 +11,48 @@ > don't need. Delete this banner once the file is real. > > Why this matters: the `session-close` skill reads this file at close time to -> run its **WIP & work-tracking reconciliation gate** — the step that makes your +> run its **WIP & work-tracking reconciliation gate**, the step that makes your > tickets / time / board reflect what the session actually did before the > session is gone. This file is the *only* place that gate learns your systems; > it hardcodes nothing. If a section is empty, the gate degrades gracefully for -> that surface (see "If nothing here is configured" at the bottom) — it never +> that surface (see "If nothing here is configured" at the bottom). It never > fails, and it never invents a system you don't have. -**EXAMPLE — replace on bootstrap:** +**EXAMPLE, replace on bootstrap:** - **Mechanism:** tool. - **Tool:** `psa__log_time` on my PSA MCP server. Fixed args it always needs: `resource = ""`, `work_type = "regular"`. The gate proposes hours + a note; I confirm before it calls the tool. -- **Companion note tool:** `psa__add_note` — attach a short summary of what +- **Companion note tool:** `psa__add_note`: attach a short summary of what shipped to the same work item (pull it from commit subjects, not "worked on stuff"). - **"Already logged today" check:** before proposing anything, ask my time system what's already recorded for today (tool `psa__time_overview` for my user id) so a day captured elsewhere never gets double-logged. - **Billable vs not:** client work is billable; internal/overhead work is - logged for tracking but not billed; personal-side work takes no entry — note - "personal — skipped" and move on. Some work is genuinely non-trackable — ask, + logged for tracking but not billed; personal-side work takes no entry, so note + "personal, skipped" and move on. Some work is genuinely non-trackable, so ask, don't assume. -**EXAMPLE — replace on bootstrap:** +**EXAMPLE, replace on bootstrap:** - **System:** my PSA's ticket module. -- **Mechanism:** tool — `psa__find_ticket` to locate the open item for a work +- **Mechanism:** tool. `psa__find_ticket` to locate the open item for a work area, `psa__create_ticket` to open one when none exists. -- **Resolution rule:** a repo is NOT 1:1 with a ticket — phase-scoped work +- **Resolution rule:** a repo is NOT 1:1 with a ticket: phase-scoped work spawns new tickets. The gate proposes a candidate and I confirm it's the right OPEN item; it never auto-picks and never terminally closes an item on my behalf. -- **New labor with no item:** open the item FIRST, then log against it — never +- **New labor with no item:** open the item FIRST, then log against it, and never log orphaned work (see the principle in the gate). -**EXAMPLE — replace on bootstrap:** +**EXAMPLE, replace on bootstrap:** -- **Board:** "Active Projects" kanban. Tool `board__update_card` — advance +- **Board:** "Active Projects" kanban. Tool `board__update_card` to advance percent + lifecycle label + a title stamp when a session moves a card. Keep this write separate from the time entry. - **Personal task tracker:** my personal to-do app. Tool `todo__complete` / `todo__edit` to tick or update sidequest items touched this session. -- **Blockers:** when work is blocked, mark the card `Waiting — Blocked` and add +- **Blockers:** when work is blocked, mark the card `Waiting: Blocked` and add a note naming the block, rather than advancing it. -**EXAMPLE — replace on bootstrap:** +**EXAMPLE, replace on bootstrap:** | Repo / work area | Ticket / item | Board card | Notes | |------------------|---------------|------------|-------| | example-webapp | TCK-1042 | Webapp | (learned 2026-01-15) | -| example-infra | TCK-0998 | Infra | phase work spawns new tickets — confirm | +| example-infra | TCK-0998 | Infra | phase work spawns new tickets, so confirm | ## If nothing here is configured -The reconciliation gate **still runs** — configuration only changes *how* it +The reconciliation gate **still runs**: configuration only changes *how* it writes, never *whether* it reconciles. With a surface left blank, the gate: - assembles the session's work picture from the work-log + git delta anyway; @@ -123,6 +123,6 @@ writes, never *whether* it reconciles. With a surface left blank, the gate: - reminds you, in the closeout receipt, to log it yourself in whatever system you use. -A blank config is a valid, supported state — the gate degrades to "draft and +A blank config is a valid, supported state: the gate degrades to "draft and remind," never to silence. Fill a section in only when wiring a tool earns the automation. diff --git a/DELIVERABLES/README.md b/DELIVERABLES/README.md index b0b099f..3c01785 100644 --- a/DELIVERABLES/README.md +++ b/DELIVERABLES/README.md @@ -1,14 +1,14 @@ # DELIVERABLES/ Home for **cross-cutting one-off deliverables** that aren't tied to a -single project — strategy, research, and analysis documents the Operator +single project: strategy, research, and analysis documents the Operator commissioned that don't belong to one repo (e.g. a portfolio-wide constraint analysis, a cross-cluster research report), plus personal deliverables that aren't notes and aren't repo-bound code. Anything scoped to one repo belongs in that repo (`PROJECTS///`); personal notes and knowledge belong in the Obsidian vault (`NOTES/MASTER/`). Filing rule: if you can name the one project repo a -file belongs to, it does NOT go here — move it. +file belongs to, it does NOT go here: move it. This folder is tracked in git so deliverables travel across machines. Naming: `kebab-case.md` (or a native extension like `.docx` for diff --git a/NOTES/MASTER/README.md b/NOTES/MASTER/README.md index a0cbd88..c5cd866 100644 --- a/NOTES/MASTER/README.md +++ b/NOTES/MASTER/README.md @@ -1,7 +1,7 @@ -# NOTES/MASTER/ — the Obsidian vault +# NOTES/MASTER/: the Obsidian vault `NOTES/MASTER/` is the Obsidian vault slot. Point Obsidian at **this -directory** (`~/OPS/NOTES/MASTER/`) when you open the vault — its +directory** (`~/OPS/NOTES/MASTER/`) when you open the vault: its `.obsidian/` config lives here, and `.gitignore` is tuned to track the vault's markdown and a curated slice of its `.obsidian/` settings while ignoring high-churn workspace state. diff --git a/PROJECTS/sync-check.sh b/PROJECTS/sync-check.sh index e324e0a..1f7ef3c 100755 --- a/PROJECTS/sync-check.sh +++ b/PROJECTS/sync-check.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# sync-check.sh — walk PROJECTS/ git repos, fetch from remote, rebase, +# sync-check.sh: walk PROJECTS/ git repos, fetch from remote, rebase, # and shepherd uncommitted changes safely through the update. # Linux port of Sync-Check.ps1. # @@ -74,7 +74,7 @@ read_choice() { declare -a sum_synced=() sum_clean=() sum_attn=() sum_skipped=() sum_stashes=() echo -status "GIT SYNC — $(date '+%Y-%m-%d %H:%M')" "$C_CYAN$C_BOLD" +status "GIT SYNC: $(date '+%Y-%m-%d %H:%M')" "$C_CYAN$C_BOLD" status "$SEPARATOR" "$C_GRAY" echo "What this does: for each project, it checks if the server has" echo "newer changes, then updates your local copy. If you have unsaved" @@ -103,7 +103,7 @@ for dir in "${REPOS[@]}"; do status "[$name]" "$C_YELLOW$C_BOLD" action "Asking the server what's new..." - explain "This doesn't change anything yet — it just downloads info." + explain "This doesn't change anything yet; it just downloads info." if ! fetch_out=$(git -C "$dir" fetch --all --prune 2>&1); then bad "Couldn't reach the server: $fetch_out" sum_skipped+=("$name") @@ -154,7 +154,7 @@ for dir in "${REPOS[@]}"; do continue fi - # Not behind — report and move on (match Windows: don't stash when there's nothing to rebase against) + # Not behind: report and move on (match Windows: don't stash when there's nothing to rebase against) if [[ "$behind" -eq 0 ]]; then if [[ "$ahead" -gt 0 ]]; then good "You're ahead of the server. No rebase needed." @@ -173,7 +173,7 @@ for dir in "${REPOS[@]}"; do # Auto mode: never touch a dirty tree that needs a rebase if [[ $AUTO -eq 1 && $is_dirty -eq 1 ]]; then - warn "Dirty tree + behind. Auto mode skips this — run interactively to resolve." + warn "Dirty tree + behind. Auto mode skips this, so run interactively to resolve." sum_attn+=("$name (dirty + behind, auto-skipped)") echo continue @@ -224,7 +224,7 @@ for dir in "${REPOS[@]}"; do fi fi - sum_attn+=("$name (rebase conflict — manual resolution needed)") + sum_attn+=("$name (rebase conflict, manual resolution needed)") echo continue fi @@ -238,7 +238,7 @@ for dir in "${REPOS[@]}"; do decided=0 while [[ $decided -eq 0 ]]; do choice=$(read_choice "Your shelved changes are still tucked away. What do you want to do with them?" \ - "P:Put them back in the project (most common — you keep working)" \ + "P:Put them back in the project (most common: you keep working)" \ "K:Leave them on the shelf for later (see them with: git stash list)" \ "D:Discard them permanently (you don't want these edits anymore)" \ "V:Show me the shelved changes first, then ask again") @@ -250,7 +250,7 @@ for dir in "${REPOS[@]}"; do good "Done. Your edits are back in place." else warn "Putting them back caused a conflict with the newly pulled code." - explain "Your edits and the server's updates touched the same lines. The shelf is still safe — resolve the conflict in your editor, or run: git stash drop $stash_ref to toss them." + explain "Your edits and the server's updates touched the same lines. The shelf is still safe: resolve the conflict in your editor, or run: git stash drop $stash_ref to toss them." sum_stashes+=("$name ($stash_label)") sum_attn+=("$name (stash pop conflict)") fi diff --git a/SKILLS/design-method/brand.md b/SKILLS/design-method/brand.md index 10cf6f5..5b3997d 100644 --- a/SKILLS/design-method/brand.md +++ b/SKILLS/design-method/brand.md @@ -1,5 +1,5 @@ -EXAMPLE — replace: web surfaces are dark-first (cinematic, restrained, surgical +EXAMPLE, replace: web surfaces are dark-first (cinematic, restrained, surgical warm accents); office documents invert to a light palette. Never mix them. ## Color tokens @@ -31,7 +31,7 @@ warm accents); office documents invert to a light palette. Never mix them. etc.), per mode. This is the source of truth for the design skill; it invents no color you do not list here. --> -EXAMPLE — replace: `--bg:#0E0F12` `--surface:#16181D` `--text:#EDEDED` +EXAMPLE, replace: `--bg:#0E0F12` `--surface:#16181D` `--text:#EDEDED` `--muted:#9AA0A6` `--accent:#`. Accent is surgical, never a large fill. ## Type stack @@ -39,7 +39,7 @@ EXAMPLE — replace: `--bg:#0E0F12` `--surface:#16181D` `--text:#EDEDED` -EXAMPLE — replace: `` (display/UI), `` (body), +EXAMPLE, replace: `` (display/UI), `` (body), `` (accents + section headings), `` (data/labels). ## Brand language and ratified patterns @@ -48,7 +48,7 @@ EXAMPLE — replace: `` (display/UI), `` (body), design skill treats them as ratified here rather than avoiding them. Also the voice rules that bind design copy (e.g. no em dashes, sentence case). --> -EXAMPLE — replace: one serif-italic accent word per heading; uppercase mono +EXAMPLE, replace: one serif-italic accent word per heading; uppercase mono eyebrows; numbered cards. No em dashes in any brand-voice output. No emoji. ## Assets and naming @@ -56,7 +56,7 @@ eyebrows; numbered cards. No em dashes in any brand-voice output. No emoji. -EXAMPLE — replace: public name ""; legal name only where required; +EXAMPLE, replace: public name ""; legal name only where required; logo used as-is from ``, never edited. ## Traps and gotchas @@ -64,5 +64,5 @@ logo used as-is from ``, never edited. -EXAMPLE — replace: HTML email header logo via MSO conditionals; CTA padding on +EXAMPLE, replace: HTML email header logo via MSO conditionals; CTA padding on the anchor with mso-padding-alt matched; verified on a real client screenshot. From 0e5f8077c7d88a9e621983b7f2129040f7594f23 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:35:54 -0400 Subject: [PATCH 02/19] ci: add the gate workflow The repo had no CI, so a broken script could merge unnoticed. gate.yml runs on every push and pull request, with no secrets, so a fork's pull request gets the same run: - syntax: bash -n, node --check, python3 -m py_compile and a PowerShell parse over every tracked script, found by extension or #! line; an empty list fails the job so a broken discovery cannot pass as clean. - suites: every suite under bin/tests, the *-selftest scripts and the two PowerShell suites; destructive ones run inside sandbox-run (bubblewrap from apt, with a probe that fails loud if the sandbox cannot be built). - smoke: a fresh install in a throwaway HOME with the current Stage-1 settings, a git identity and deploy.sh --no-system run twice, then verify-ops.sh. - secrets: gitleaks over the full history from a binary pinned by SHA-256 (the official action needs a license key for organization repositories), then OPS's own secrets-scan.sh. Actions are pinned by commit. Every job is skipped outside the template repository: a private copy inherits this file, and its daily backup push would otherwise spend runner minutes on a gate written for the template. The image is ubuntu-24.04, not ubuntu-latest, because the sandbox tests depend on how the distribution's AppArmor treats bubblewrap. The PowerShell parse step runs a parser file with -File: passing the path after -Command makes pwsh append it to the command text and run each script (deploy.ps1 included) instead of parsing it. --- .github/workflows/gate.yml | 301 +++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 .github/workflows/gate.yml diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml new file mode 100644 index 0000000..80fc0ab --- /dev/null +++ b/.github/workflows/gate.yml @@ -0,0 +1,301 @@ +# The OPS gate. Runs on every push and pull request: +# +# syntax bash -n, node --check, python3 -m py_compile and a PowerShell parse +# over every tracked script +# suites every suite under .claude-config/bin/tests/, the *-selftest scripts +# and the PowerShell suites (the ones that delete or move files run +# inside .claude-config/bin/sandbox-run, as CONTRIBUTING.md says) +# smoke a fresh install in a throwaway HOME (the current Stage-1 settings, +# a git identity, deploy.sh --no-system, twice), then verify-ops.sh +# secrets gitleaks over the full history, then OPS's own secrets scanner +# +# No secrets are used or needed, so a pull request from a fork gets the same +# run. A fork's token is read-only (permissions below) and nothing here asks for +# another. +# +# Every job is skipped outside the template repository. A private copy made +# from the template inherits this file (and template sync can deliver it), and +# its daily backup pushes every day: without the guard each push would spend +# runner minutes on a gate written for the template. Delete the file from your +# copy if you like; if this repository is ever renamed, update the guard, or +# the gate silently stops running. +# +# Not covered, on purpose: Windows and macOS (this runs on Linux; the two +# PowerShell suites run under pwsh on Linux and exercise deploy.ps1's logic, not +# a Windows host), and any Claude Code CLI behavior (the CLI is not installed; +# deploy warns and goes on without it). `.claude-config/bin/README.md` and +# CONTRIBUTING.md list the same commands for a local run. +name: gate + +on: + push: + pull_request: + +permissions: + contents: read + +# A newer push to the same ref supersedes a run still going. +concurrency: + group: gate-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + # bash --noprofile --norc -eo pipefail: a failing command fails the step. + shell: bash + +env: + # Stage 1's settings.json, read from the public default branch (HEAD follows a + # rename of it). Deliberately not pinned to a commit: the smoke test and the + # verify-ops self-test exist to catch the day a Stage-1 change (a model pin, a + # hook matcher) breaks OPS, so they must read the file new users get today. + OPS_STAGE1_SETTINGS_URL: https://raw.githubusercontent.com/Exploitacious/linuxploitacious/HEAD/claude/.claude/settings.json + +jobs: + syntax: + name: syntax + if: github.repository == 'Exploitacious/OPS' + # A named image, not ubuntu-latest: the sandbox tests depend on how Ubuntu's + # AppArmor treats bubblewrap, and a silent image change should not move them. + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: List the scripts to check + # Tracked files only, found by extension or by a #! line (branch-gc, + # grabit, memory-init and other scripts have no extension). A check that + # sees zero files would pass without checking anything, so an empty list + # fails the job. + run: | + : > "$RUNNER_TEMP/shell.lst"; : > "$RUNNER_TEMP/node.lst" + : > "$RUNNER_TEMP/python.lst"; : > "$RUNNER_TEMP/pwsh.lst" + while IFS= read -r -d '' f; do + [ -f "$f" ] && [ ! -L "$f" ] || continue + case "$f" in + *.sh) echo "$f" >> "$RUNNER_TEMP/shell.lst" ;; + *.js) echo "$f" >> "$RUNNER_TEMP/node.lst" ;; + *.py) echo "$f" >> "$RUNNER_TEMP/python.lst" ;; + *.ps1) echo "$f" >> "$RUNNER_TEMP/pwsh.lst" ;; + *) + first="$(head -n 1 "$f" | tr -d '\r')" + case "$first" in + '#!'*bash*|'#!/bin/sh'*|'#!/usr/bin/env sh'*) echo "$f" >> "$RUNNER_TEMP/shell.lst" ;; + '#!'*python*) echo "$f" >> "$RUNNER_TEMP/python.lst" ;; + '#!'*node*) echo "$f" >> "$RUNNER_TEMP/node.lst" ;; + esac ;; + esac + done < <(git ls-files -z) + for k in shell node python pwsh; do + n="$(wc -l < "$RUNNER_TEMP/$k.lst")" + echo "$k: $n file(s)" + [ "$n" -gt 0 ] || { echo "::error::no $k scripts found: the discovery is broken"; exit 1; } + done + + - name: bash -n on every shell script + run: | + rc=0 + while IFS= read -r f; do + bash -n "$f" || { echo "::error file=$f::bash -n failed"; rc=1; } + done < "$RUNNER_TEMP/shell.lst" + exit "$rc" + + - name: node --check on every .js + run: | + node --version + rc=0 + while IFS= read -r f; do + node --check "$f" || { echo "::error file=$f::node --check failed"; rc=1; } + done < "$RUNNER_TEMP/node.lst" + exit "$rc" + + - name: python3 -m py_compile on every .py + # Bytecode goes to a scratch prefix, not into the checkout. + run: | + python3 --version + export PYTHONPYCACHEPREFIX="$RUNNER_TEMP/pycache" + rc=0 + while IFS= read -r f; do + python3 -m py_compile "$f" || { echo "::error file=$f::py_compile failed"; rc=1; } + done < "$RUNNER_TEMP/python.lst" + exit "$rc" + + - name: Parse every PowerShell script + # Parse only: ParseFile never runs the script. The parser is a file run + # with -File so the path arrives as a bound parameter; passing it after + # -Command makes pwsh append it to the command text, which would RUN + # each script (deploy.ps1 and Sync-Check.ps1 included) instead of parsing it. + run: | + pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' + cat > "$RUNNER_TEMP/parse.ps1" <<'PS1' + param([Parameter(Mandatory)][string]$Path) + $errs = $null; $tokens = $null + [void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path -LiteralPath $Path).Path, [ref]$tokens, [ref]$errs) + if ($errs.Count) { $errs | ForEach-Object { "${Path}: $($_.Message)" }; exit 1 } + PS1 + rc=0 + while IFS= read -r f; do + pwsh -NoProfile -NonInteractive -File "$RUNNER_TEMP/parse.ps1" "$f" \ + || { echo "::error file=$f::PowerShell parse failed"; rc=1; } + done < "$RUNNER_TEMP/pwsh.lst" + exit "$rc" + + suites: + name: suites + if: github.repository == 'Exploitacious/OPS' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install bubblewrap, zsh and rsync, and prove bubblewrap can build a sandbox + # sandbox-run needs a root-owned bwrap. One root-wrapper case runs in zsh + # (it prints SKIP without it), and the skill-mount tests exercise the + # rsync engine as well as the tar fallback. Ubuntu 24.04 can restrict + # unprivileged user namespaces through AppArmor; if the plain probe + # fails, lift that restriction for this runner and probe again. The last + # probe is not optional: without a working sandbox the destructive-tool + # suites cannot run, and skipping them would hide a broken gate. + run: | + need=() + for pkg in bubblewrap:bwrap zsh:zsh rsync:rsync; do + command -v "${pkg#*:}" >/dev/null 2>&1 || need+=("${pkg%%:*}") + done + if [ "${#need[@]}" -gt 0 ]; then + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends "${need[@]}" + fi + bwrap --version + if ! bwrap --ro-bind / / true 2>/dev/null; then + echo "bwrap is blocked here; lifting the AppArmor user-namespace restriction" + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + fi + bwrap --ro-bind / / true + + - name: Fetch the Stage-1 settings for the verify-ops self-test + # verify-ops-selftest.sh also runs the shipped tree against a real + # Stage-1 file when OPS_SELFTEST_STAGE1 names one; without it that case + # prints SKIP. + run: curl -fsSL --retry 3 --retry-delay 2 "$OPS_STAGE1_SETTINGS_URL" -o "$RUNNER_TEMP/stage1-settings.json" && jq -e . "$RUNNER_TEMP/stage1-settings.json" >/dev/null + + - name: Suites under .claude-config/bin/tests + run: | + fails=0 + for t in .claude-config/bin/tests/*-test.sh .claude-config/bin/tests/*-test.py; do + case "$t" in *.py) run=(python3 -B) ;; *) run=(bash) ;; esac + echo "::group::$t" + case "$t" in + # These two start sandbox-run themselves and bwrap cannot nest, so + # they run outside it (safe by construction: each writes and + # deletes only in its own scratch directory). + */sandbox-run-test.sh|*/sandbox-run-fd-test.py) + "${run[@]}" "$t" || { echo "FAILED: $t"; fails=$((fails + 1)); } ;; + *) + .claude-config/bin/sandbox-run --no-bind --no-net --ro-bind "$PWD" -- "${run[@]}" "$t" \ + || { echo "FAILED: $t"; fails=$((fails + 1)); } ;; + esac + echo "::endgroup::" + done + echo "suites failed: $fails" + [ "$fails" -eq 0 ] + + - name: Self-tests beside their subjects + run: | + fails=0 + for t in .claude-config/bin/*-selftest.* .claude-config/hooks/*-selftest.sh; do + case "$t" in *.py) run=(python3 -B) ;; *) run=(bash) ;; esac + echo "::group::$t" + OPS_SELFTEST_STAGE1="$RUNNER_TEMP/stage1-settings.json" \ + .claude-config/bin/sandbox-run --no-bind --no-net --ro-bind "$PWD" \ + --ro-bind "$RUNNER_TEMP/stage1-settings.json" -- "${run[@]}" "$t" \ + || { echo "FAILED: $t"; fails=$((fails + 1)); } + echo "::endgroup::" + done + echo "self-tests failed: $fails" + [ "$fails" -eq 0 ] + + - name: PowerShell suites (pwsh on Linux) + run: | + fails=0 + for t in deploy-windows-test.ps1 memory-init-windows-test.ps1; do + echo "::group::$t" + pwsh -NoProfile -File ".claude-config/bin/tests/$t" || { echo "FAILED: $t"; fails=$((fails + 1)); } + echo "::endgroup::" + done + [ "$fails" -eq 0 ] + + smoke: + name: fresh install, then verify-ops + if: github.repository == 'Exploitacious/OPS' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Deploy into a throwaway HOME, then run the drift gate + # One step, so HOME stays the throwaway for every command. This is what a + # new user's host looks like after Stage 1: the Stage-1 settings file in + # ~/.claude, a git identity, this repository at ~/OPS. --no-system keeps + # deploy from touching the runner's crontab or root's rc files. Deploy + # runs twice, because the docs promise it is idempotent; verify-ops.sh + # exits 1 on any FAIL. + run: | + T="$(mktemp -d "$RUNNER_TEMP/ops-smoke.XXXXXX")" + export HOME="$T" + unset CLAUDE_CONFIG_DIR + git config --global user.name "OPS gate" + git config --global user.email "ops-gate@example.invalid" + mkdir -p "$HOME/.claude" + curl -fsSL --retry 3 --retry-delay 2 "$OPS_STAGE1_SETTINGS_URL" -o "$HOME/.claude/settings.json" + jq -e . "$HOME/.claude/settings.json" >/dev/null + git clone --quiet "$GITHUB_WORKSPACE" "$HOME/OPS" + echo "::group::deploy.sh --no-system (first run)" + bash "$HOME/OPS/.claude-config/deploy.sh" --no-system + echo "::endgroup::" + echo "::group::deploy.sh --no-system (second run)" + bash "$HOME/OPS/.claude-config/deploy.sh" --no-system + echo "::endgroup::" + [ "$(readlink -f "$HOME/.claude/rules/ops-core.md")" = "$HOME/OPS/CONTEXT/core.md" ] + bash "$HOME/OPS/.claude-config/bin/verify-ops.sh" | tee "$T/verify.out" + tail -n 1 "$T/verify.out" | grep -q ' 0 fail ' + + secrets: + name: secrets + if: github.repository == 'Exploitacious/OPS' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Check out the full history + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: gitleaks over the full history + # The binary, not gitleaks-action: the action wants a license key for + # organization-owned repositories, and a fork's pull request has no + # secrets to give it. The tarball is checked against a pinned SHA-256 + # (the value published in the release's checksums file) before it runs. + env: + GITLEAKS_VERSION: 8.30.1 + GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb + run: | + curl -fsSL --retry 3 --retry-delay 2 \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + -o "$RUNNER_TEMP/gitleaks.tgz" + echo "${GITLEAKS_SHA256} $RUNNER_TEMP/gitleaks.tgz" | sha256sum -c - + tar -xzf "$RUNNER_TEMP/gitleaks.tgz" -C "$RUNNER_TEMP" gitleaks + "$RUNNER_TEMP/gitleaks" version + "$RUNNER_TEMP/gitleaks" git --redact --no-banner --verbose "$GITHUB_WORKSPACE" + + - name: OPS's own secrets scanner over memory, lessons and skills + run: .claude-config/bin/secrets-scan.sh CONTEXT SKILLS .claude-config From a899975c1cd30e7cd49ade02cc484afcb5579c99 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:36:12 -0400 Subject: [PATCH 03/19] docs(skills): add third-party notices One file lists every work the skills adapt, with its author, license, what OPS took and where the full license text lives: grill-me, tdd and diagnosing-bugs from mattpocock/skills (MIT), the writing model meta-skill-creator names from the same repository, the communication defaults adapted from i-have-adhd (MIT), the AI-tell catalog adapted from cursor/plugins unslop (MIT), and the design method adapted from anthropics/skills frontend-design (Apache-2.0). The design method carried the Apache attribution in one line with no copy of the license, which section 4 of that license asks for; the full text is now an appendix, with the MIT text of mattpocock/skills beside it. Both are reproduced verbatim from the upstream files. The file lives under SKILLS/ because every adapted work does, and the root of this repository holds only the canonical files that verify-ops.sh check 1 allows. --- SKILLS/THIRD_PARTY_NOTICES.md | 306 ++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 SKILLS/THIRD_PARTY_NOTICES.md diff --git a/SKILLS/THIRD_PARTY_NOTICES.md b/SKILLS/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..0cf3e2b --- /dev/null +++ b/SKILLS/THIRD_PARTY_NOTICES.md @@ -0,0 +1,306 @@ +# Third-party notices + +OPS is released under the MIT License (`LICENSE`, Copyright (c) 2026 +Exploitacious). Some skills adapt work by other people, each under a license of +its own. This file lists every such source, what OPS took from it, and where the +full license text lives. It is a good-faith record, not legal advice: if you +redistribute OPS or reuse these skills, read the upstream licenses yourself. + +Nothing else in this repository is vendored from a third party. OPS calls other +tools (git, gh, jq, bubblewrap, the Claude Code CLI, and gitleaks in CI) but +neither ships nor modifies them. The frameworks `CONTEXT/how-i-work.md` names +(Theory of Constraints, the Improvement Kata, the Phoenix Project lens) are +referenced ideas; no text from those works is copied. + +## Summary + +| Source | Author | License | What OPS took | Where it lives in OPS | Full license text | +|---|---|---|---|---|---| +| [mattpocock/skills](https://github.com/mattpocock/skills), `grilling` | Matt Pocock | MIT | Adapted into the `grill-me` skill | `SKILLS/grill-me/` (mounted) | `LICENSE.upstream` in that folder; Appendix A | +| mattpocock/skills, `tdd` and `diagnosing-bugs` | Matt Pocock | MIT | Vendored; maintained in `agent-skills` | `SKILLS/tdd/`, `SKILLS/diagnosing-bugs/` (mounted) | `LICENSE.upstream` in each folder; Appendix A | +| mattpocock/skills, `writing-for-agents` | Matt Pocock | MIT | Its writing model, adopted by `meta-skill-creator` (named in that skill's text) | `SKILLS/meta-skill-creator/` (mounted) | Appendix A; the upstream repository | +| [ayghri/i-have-adhd](https://github.com/ayghri/i-have-adhd) | Ayoub Ghriss | MIT | Adapted into the operating model's communication defaults | `SKILLS/operating-model/references/adhd-output.md` | The same file, "Upstream MIT license" | +| [cursor/plugins](https://github.com/cursor/plugins), `pstack/skills/unslop` | Lauren Tan | MIT | Adapted into the AI-tell catalog | `SKILLS/operating-model/references/patterns.md` | The same file, "Upstream MIT license" | +| [anthropics/skills](https://github.com/anthropics/skills), `frontend-design` | Not named in the license file | Apache-2.0 | The design method, adapted | `SKILLS/design-method/SKILL.md` | Appendix B | + +"Mounted" means the folder is not tracked in this repository: deploy clones +[`Exploitacious/agent-skills`](https://github.com/Exploitacious/agent-skills) +(MIT, Copyright (c) 2026 Exploitacious) and `skills-vendor.sh sync` mirrors it +into `SKILLS/` (`SKILLS/VENDORED.tsv`). The `LICENSE.upstream` files travel +inside those folders, so a mirror carries them. + +## What each license asks + +**MIT (every source except `frontend-design`).** The license grants use, copying, +modification, and distribution, on one condition quoted from its text: "The above +copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software." It also disclaims all warranty. OPS keeps +that notice with each adapted work, as the summary table shows. + +**Apache-2.0 (`frontend-design`).** Section 4 of the license asks anyone who +redistributes the work or a derivative to (a) give recipients a copy of the +License, (b) mark modified files with a prominent notice that they were changed, +(c) keep the copyright, patent, trademark, and attribution notices from the +source, and (d) carry over the attribution text of a NOTICE file, if the work +ships one. The upstream skill ships `LICENSE.txt` and `SKILL.md` only, with no +NOTICE file. + +## Details + +### mattpocock/skills + +- Upstream: https://github.com/mattpocock/skills (MIT, Copyright (c) 2026 Matt + Pocock). +- `grill-me` is adapted from the `grilling` skill at commit `85f83d3fde1d`, + rewritten for this library (no emojis, a structured question tool where the + harness has one, a recommended answer on every question, facts gathered by the + agent, and model invocation left on so it fires at intake). +- `tdd` and `diagnosing-bugs` are vendored at commit `6654f6b60cd9` and + maintained in `agent-skills`. +- `meta-skill-creator` states that its writing model comes from the upstream + `writing-for-agents` skill (context pointers, the two loads, the information + hierarchy). That folder does not bundle the upstream notice, so Appendix A + carries it. +- The upstream notice is bundled as `LICENSE.upstream` in `grill-me`, `tdd`, and + `diagnosing-bugs`, and reproduced in Appendix A. + +### ayghri/i-have-adhd + +- Upstream: https://github.com/ayghri/i-have-adhd (MIT, Copyright (c) 2026 Ayoub + Ghriss), reviewed at commit `ff690b6fbd3383e4fce6a7e50c7b409ca9a6a804`. +- OPS adapted the file `skills/i-have-adhd/SKILL.md` into the operating model's + communication defaults. `adhd-output.md` records what was kept, what changed, + and carries the upstream license in full under "Upstream MIT license". + +### cursor/plugins, pstack/skills/unslop + +- Upstream: https://github.com/cursor/plugins, under `pstack` (MIT, Copyright + (c) 2026 Lauren Tan; the license file is `pstack/LICENSE`), adapted at commit + `fd878692de15`. +- OPS adapted the catalog of AI-writing tells. `patterns.md` states the + provenance and carries the upstream license in full under "Upstream MIT + license". Re-review upstream before any version bump, as that file says. + +### anthropics/skills, frontend-design + +- Upstream: https://github.com/anthropics/skills, `skills/frontend-design` + (Apache-2.0; the license file is `skills/frontend-design/LICENSE.txt`, which + names no copyright holder). The adapted version is not pinned to a commit. +- OPS rewrote the design method for this template and added a brand-layer slot + (`brand.md`) of its own. `SKILLS/design-method/SKILL.md` says the method is + adapted from the upstream skill, which is the notice that the file was + changed. The full License text is Appendix B. + +## Appendix A: MIT License, mattpocock/skills + +Reproduced verbatim from the `LICENSE` file at the root of +https://github.com/mattpocock/skills. + +```text +MIT License + +Copyright (c) 2026 Matt Pocock + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## Appendix B: Apache License 2.0, anthropics/skills frontend-design + +Reproduced verbatim from `skills/frontend-design/LICENSE.txt` in +https://github.com/anthropics/skills. + +```text + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +``` From 754ed60ede7f3420395cf66106add83a3a0a24b7 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:44:14 -0400 Subject: [PATCH 04/19] docs: match the README, deployment and contributing docs to the code Every claim was checked against the scripts or by running the command. README: requirements (git with an identity, a signed-in gh, python3, jq), the T3 Code path, and sections on what deploy does, the push gate and its limits, the daily backup, skill mounts, template sync and how to check a copy. The git guard's list now includes a bare tmux kill-server and an unverified push. DEPLOYMENT: the trust anchor is ~/.claude.json, beside ~/.claude/, not ~/.claude/.claude.json as written; a failed skill clone fails the deploy (skills-mount) rather than warning; the verification block gains the exact origin-private-check line, a git identity check and the verify-ops summary line; the machinery deploy does not wire is three pieces, not two; and a new section gives the throwaway-HOME recipe that the CI smoke job runs. CONTRIBUTING: the private-term rule, where destructive tests run and why, the GIT_GUARD=off rule for a PR branch, and the full suite command. The old loop printed FAILED but always exited 0; this one counts failures, exits non-zero, and covers the self-tests and the PowerShell suites. verify-ops.sh on a bare checkout reads host state from HOME and fails on a machine with no deployed OPS, so the docs now point at the throwaway-HOME run. AGENTS and BOOTSTRAP: the CI pointer, the same verify-ops caveat, the notices file, and a git identity probe in the bootstrap recon. --- AGENTS.md | 16 +++- BOOTSTRAP.md | 5 ++ CONTRIBUTING.md | 136 ++++++++++++++++++++++++++----- DEPLOYMENT.md | 52 ++++++++++-- README.md | 212 +++++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 382 insertions(+), 39 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4c0789e..3357c8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,14 +27,20 @@ current task allows. ## Build, test, land - Gate: `bash .claude-config/bin/verify-ops.sh` after any harness change. It - checks `~/OPS`; set `OPS_DIR` to check another checkout. + checks `~/OPS`; set `OPS_DIR` to check another checkout. Its host checks + (the core link, the Stage-1 settings) still read `$HOME`, so on a machine + with no deployed OPS run it in a throwaway `HOME` (DEPLOYMENT.md, "Testing a + deploy under a throwaway HOME"). - Tests: `.claude-config/bin/tests/*-test.{sh,py}`, `.claude-config/bin/*-selftest.*`, and `.claude-config/hooks/*-selftest.sh`. Run the ones covering what you changed. A suite that runs a tool that deletes or moves files starts with the sandbox guard and runs only inside `.claude-config/bin/sandbox-run` (exit 97 elsewhere); the command line is in `.claude-config/bin/README.md`. Every shell script you touch passes `bash -n`; every workflow script passes - `node --check`. + `node --check`. In the template repository, CI + (`.github/workflows/gate.yml`) runs the syntax checks, every suite, a + fresh-install smoke test with `verify-ops.sh`, and a secrets scan on each push + and pull request; CONTRIBUTING.md has the same commands for a local run. - `CHANGELOG.md` gets a dated entry for every notable change. - Several sessions can share this checkout. Before pushing, run `git log --oneline origin/main..main`; if it shows commits that are not @@ -45,7 +51,9 @@ current task allows. here and blocks one it cannot verify, or one to another remote. It is a seatbelt, not a sandbox: DEPLOYMENT.md "Push gate" lists its limits. - Contributions to the public template go by PR and follow - [CONTRIBUTING.md](CONTRIBUTING.md): port the mechanic, never paste the file. + [CONTRIBUTING.md](CONTRIBUTING.md): port the mechanic, never paste the file, + keep your private-term list out of every PR and commit, and push the PR branch + with `GIT_GUARD=off` (the git guard blocks an unverified push by design). ## Conventions @@ -85,4 +93,6 @@ current task allows. | First launch | `BOOTSTRAP.md` | | Template sync from upstream | the `harness-update` skill | | Scripts, tests, and the gate | `.claude-config/bin/README.md` | +| Porting rules, the CI gate, local test commands | `CONTRIBUTING.md`, `.github/workflows/gate.yml` | +| Licenses of adapted third-party work | `SKILLS/THIRD_PARTY_NOTICES.md` | | History | `CHANGELOG.md`, `git log` | diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 7c62ada..a8f3160 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -51,8 +51,13 @@ anything wrong"), so recon is transparent. - **Identity the box already has:** ```bash git config --get user.name; git config --get user.email + git var GIT_AUTHOR_IDENT # an error means every commit fails gh auth status 2>&1 | head ``` + If `git var` errors, tell the Operator to run `git config --global user.name` + and `user.email`: deploy fails with `git-identity` until they are set + (`DEPLOYMENT.md`, step 7), and a missing identity breaks the memory sync and + the daily backup. - **Installed toolchain** (probe presence, install nothing): ```bash for t in docker node python3 pwsh go rustc psql terraform kubectl; do \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c28147f..b95b007 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,25 @@ how private strings survive into public commits. Instead: 3. Where an example is structurally required, invent one. Obviously synthetic beats realistic: realistic examples read as leaks even when they aren't. +## The private-term rule + +The list of private terms you scan for (your name, employer, clients, +teammates, hostnames, tailnet names, project codenames, ticket prefixes) is +itself private. Keep it in your own harness, and never publish it: not in a +file, a commit message, a PR title or description, an issue, or a comment. A +PR body that says "I grepped for these 12 terms" followed by the terms +publishes exactly what the scan exists to keep out. + +- Scan what you will publish, not only the files: the diff, every commit + message (`git log -p origin/main..HEAD`), and the PR title and body before you + submit it. +- Scan the history of the branch too. A term fixed in a later commit is still + in the earlier one, and a PR keeps its commits on GitHub permanently, so + squash or rewrite your own branch before you push it. +- If a private term does reach a public PR, commit, or comment, say so to the + maintainer in the PR right away, without repeating the term. Do not + force-push over it: GitHub keeps the old commits and the PR's edit history. + ## Working with an AI on the port (recommended) If you're having Claude (or any agent) do the merge, give it this brief @@ -45,42 +64,121 @@ verbatim: it is the same contract this repo was originally extracted under: ## Before you open the PR -Run the repo's own gates from your branch root: +CI runs the gate (`.github/workflows/gate.yml`) on every pull request, with no +secrets, so a fork's PR gets the same run. Run the same checks first; each is +a job in that workflow. + +**1. Syntax.** CI finds every tracked script by extension or `#!` line; check +what you touched: ```bash -.claude-config/bin/secrets-scan.sh CONTEXT SKILLS .claude-config -OPS_DIR="$PWD" .claude-config/bin/verify-ops.sh # checks this checkout (default ~/OPS) -# every suite, through the sandbox (needs bwrap; .claude-config/bin/README.md, "tests/") +bash -n +node --check +python3 -m py_compile # use PYTHONPYCACHEPREFIX=/tmp/pyc to keep __pycache__ out of the tree +``` + +**2. Every suite.** Run from your checkout root. This is the CI command; it +exits non-zero if any suite fails. It needs `bwrap` (`sudo apt install +bubblewrap`; see "Where destructive tests run" below) and, for the last loop, +`pwsh`: + +```bash +fails=0 for t in .claude-config/bin/tests/*-test.sh .claude-config/bin/tests/*-test.py; do case "$t" in *.py) run=(python3 -B) ;; *) run=(bash) ;; esac - case "$t" in */sandbox-run-test.sh|*/sandbox-run-fd-test.py) "${run[@]}" "$t" || echo "FAILED: $t"; continue ;; esac - .claude-config/bin/sandbox-run --no-bind --no-net --ro-bind "$PWD" -- "${run[@]}" "$t" || echo "FAILED: $t" + case "$t" in + */sandbox-run-test.sh|*/sandbox-run-fd-test.py) # these start sandbox-run themselves + "${run[@]}" "$t" || { echo "FAILED: $t"; fails=$((fails + 1)); } ;; + *) + .claude-config/bin/sandbox-run --no-bind --no-net --ro-bind "$PWD" -- "${run[@]}" "$t" \ + || { echo "FAILED: $t"; fails=$((fails + 1)); } ;; + esac done -bash -n -node --check +for t in .claude-config/bin/*-selftest.* .claude-config/hooks/*-selftest.sh; do + case "$t" in *.py) run=(python3 -B) ;; *) run=(bash) ;; esac + .claude-config/bin/sandbox-run --no-bind --no-net --ro-bind "$PWD" -- "${run[@]}" "$t" \ + || { echo "FAILED: $t"; fails=$((fails + 1)); } +done +for t in deploy-windows-test.ps1 memory-init-windows-test.ps1; do + pwsh -NoProfile -File ".claude-config/bin/tests/$t" || { echo "FAILED: $t"; fails=$((fails + 1)); } +done +echo "failed suites: $fails" ``` -All clean, plus your own denylist grep at zero hits. Say in the PR body that -you ran them: and expect the maintainer to run an independent leak pass -anyway before merging. Anything that looks machine-specific or reads too real -to be synthetic will get questioned; that's the review working as intended. +`verify-ops-selftest.sh` also checks the shipped tree against the real Stage-1 +settings when `OPS_SELFTEST_STAGE1` names a copy of that file; without it that +one case prints `SKIP`. CI fetches the file and sets the variable; to do the +same, download it (the URL is in `DEPLOYMENT.md`, "Testing a deploy under a +throwaway HOME"), then add `--ro-bind ` to that suite's `sandbox-run` +line and set the variable on the command. + +**3. A fresh-install smoke test, then the drift gate.** `verify-ops.sh` checks +the machine it runs on (the core link under `~/.claude/rules/`, the Stage-1 +settings), so on a machine with no deployed OPS, `OPS_DIR="$PWD" +.claude-config/bin/verify-ops.sh` reports a FAIL for the missing link, which +says nothing about your change. Run it the way CI does, in a throwaway `HOME`: +`DEPLOYMENT.md`, "Testing a deploy under a throwaway HOME" has the commands. +On a machine where OPS is already deployed, `OPS_DIR="$PWD" +.claude-config/bin/verify-ops.sh` checks your checkout directly. + +**4. Secrets and private terms.** `.claude-config/bin/secrets-scan.sh CONTEXT +SKILLS .claude-config` is OPS's own detector (it takes a couple of minutes: it +greps line by line), and CI also runs `gitleaks` over the full history. Then +run your own private-term scan at zero hits (see the private-term rule above). + +Say in the PR body that you ran them, and expect the maintainer to run an +independent leak pass anyway before merging. Anything that looks +machine-specific or reads too real to be synthetic will get questioned; that +is the review working as intended. + +## Where destructive tests run + +A test, self-test, mutation check, or probe of a tool that deletes, moves, or +overwrites files runs only inside `.claude-config/bin/sandbox-run`: a +bubblewrap sandbox with a read-only root, a fresh home directory and `/tmp`, +and nothing writable unless you name it. Why: in the private harness OPS is +ported from, an unsandboxed mutation sweep of a cleanup tool ran the real +deleter on `/` and `$HOME` and wiped most of a home directory. A throwaway +`HOME` did not help, because a mutated tool computed its own path. + +- A suite for such a tool starts with the guard line from + `.claude-config/bin/sandbox-lint`'s header and exits 97 outside the + sandbox. `verify-ops.sh` check 24 fails a suite that names one of the tools + (`OPS_DESTRUCTIVE_TOOLS` in `verify-ops.sh`) without it. Add the guard to any + new suite of that kind. +- The sandbox is Linux only (`bwrap`). On macOS or Windows, run those suites on + a Linux host, or open the PR and let CI run them. +- bubblewrap cannot nest on Ubuntu 23.10 and later, so the two suites that + start `sandbox-run` themselves (`sandbox-run-test.sh`, + `sandbox-run-fd-test.py`) run outside it; they write and delete only in their + own scratch directory. +- A mutation sweep also runs in the sandbox, with a scratch copy as the only + `--bind`. Details and the exit codes: `.claude-config/bin/README.md`, + "sandbox-run". ## Mechanics - `main` is protected: contributions land by PR with review, no direct pushes, no force pushes. -- Your clone of this repo is itself an OPS copy (it holds - `.claude-config/bin/origin-private-check`) whose origin is public, so the - git-guard hook blocks an agent's push of your PR branch, by design: that is - the push it exists to stop in a private copy. Check that the branch carries - no private content (the denylist grep above, at zero hits), then push it - with the documented one-command override: - `GIT_GUARD=off git push origin `. +- **`GIT_GUARD=off` for your PR branch.** Your clone of this repo is itself an + OPS copy (it holds `.claude-config/bin/origin-private-check`) whose origin is + public, so the git-guard hook blocks an agent's push of your PR branch, by + design: that is the push it exists to stop in a private copy. Check that the + branch carries no private content (the private-term rule, at zero hits), then + push it with the documented one-command override, which the hook honors only + at the very start of a command: + `GIT_GUARD=off git push origin `. Use it for that push and nothing + else, and never set it for the whole session. - Conventional Commits (`(): `, subject ≤ 72 chars). Body explains *why* when it isn't obvious. - One mechanic per PR beats a best-of-both-worlds mega-merge. Big merges hide leaks and stall review; a stack of small PRs flows. - Doc updates ride in the same PR as the change they describe (`CONTEXT/project-kata.md`: docs match reality, always). +- Third-party work: if your change adapts someone else's work, say so in the + PR and add it to `SKILLS/THIRD_PARTY_NOTICES.md` with its license, in the + same change. +- CI (`.github/workflows/gate.yml`) must be green before a merge. A failing job + is a defect in the change, or in the gate; do not merge around it. - This is a no-SLA repo (see README § Posture). PRs are welcome; response time isn't promised. diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 8d7c93a..88a5dde 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -139,8 +139,9 @@ In order: change into skills every session loads. Update on purpose: `git -C ~/OPS/PROJECTS/Exploitacious/agent-skills pull --ff-only`, then `~/OPS/.claude-config/bin/skills-vendor.sh sync`. A failed clone (no - network) is a warning; `verify-ops.sh` reports the mounts MISSING until a - re-run clones them. + network) prints an `[XX]` line and fails the deploy (`skills-mount`); + `verify-ops.sh` warns that the mounts are MISSING until a re-run clones + them. 3. **Link the always-loaded core** into `~/.claude/rules/ops-core.md` (see "How the core loads"). A regular file in the way is backed up under a name Claude @@ -245,8 +246,9 @@ In order: marketplace registered outside that scope stays put with a warning. Inventory and removal failures are visible warnings, never a silent success. -10. **Seed the trust anchor.** Marks `$HOME` trusted in `~/.claude/.claude.json` - (`hasTrustDialogAccepted: true`) so Claude Code's parent-dir trust walk +10. **Seed the trust anchor.** Marks `$HOME` trusted in `~/.claude.json`, which + sits beside `~/.claude/`, not inside it (`hasTrustDialogAccepted: true`), so + Claude Code's parent-dir trust walk covers every repo under `$HOME` with no folder-trust prompt. Keyed off `$HOME`; idempotent; merges into the existing file without touching other keys. A second profile via `CLAUDE_CONFIG_DIR` is not covered @@ -293,20 +295,54 @@ claude plugin list # Stage 2 left the Stage-1 settings alone (expect: no output) git -C ~/linuxploitacious status --short -- claude/.claude/settings.json -# The drift gate passes (expect: "0 fail" on the last line) +# The drift gate passes (expect: no FAIL line, and "0 fail" in the last line, +# "verify-ops: ok · warn · 0 fail ()") bash ~/OPS/.claude-config/bin/verify-ops.sh # Backup scheduler crontab -l | grep OPS # Linux schtasks /Query /TN "OPS Daily Backup" # Windows -# The backup may push (expect exit 0 and "private: github.com// is private") +# The backup may push (expect exit 0 and the line +# "origin-private-check: private: github.com// is private"; +# exit 1 is a public origin, exit 2 one it cannot verify) bash ~/OPS/.claude-config/bin/origin-private-check ~/OPS; echo "exit $?" + +# Commits can run (expect one line, "Your Name "; +# an error means every commit will fail) +git -C ~/OPS var GIT_AUTHOR_IDENT ``` Then open a new session (any launch path) and check the briefing's `Boot:` line. If all succeed, deployment is complete. +### Testing a deploy under a throwaway HOME + +To try a deploy, or a change to the deployer, without touching your real setup, +run it under a throwaway `HOME`. CI does exactly this on every pull request +(the `smoke` job in `.github/workflows/gate.yml`). The subshell keeps the new +`HOME` out of your session: + +```bash +( + export HOME="$(mktemp -d)"; unset CLAUDE_CONFIG_DIR + git config --global user.name "Throwaway" && git config --global user.email throwaway@example.invalid + mkdir -p ~/.claude + curl -fsSL https://raw.githubusercontent.com/Exploitacious/linuxploitacious/HEAD/claude/.claude/settings.json -o ~/.claude/settings.json + git clone ~/OPS + bash ~/OPS/.claude-config/deploy.sh --no-system + bash ~/OPS/.claude-config/bin/verify-ops.sh +) +``` + +The `curl` line fetches the Stage-1 settings a new host has; the git identity +is the prerequisite from step 7. Expect `Deploy complete.` and, on the last +line of the gate, `verify-ops: ok · 0 warn · 0 fail ()`, with +both commands exiting 0. `--no-system` keeps deploy off your crontab and root's +rc files. Deploy still clones the public skill source over HTTPS, so the test +needs network access, and the throwaway directory stays in your temp dir until +you delete it. + ## T3 Code hosts T3 Code runs Claude Code as a provider thread, so nothing on the OPS boot path @@ -336,7 +372,7 @@ terminal option does not reach T3 threads; the managed-settings option does. ## Hooks and the drift gate (activated outside Stage 2) -Two pieces of OPS machinery ship in the repo but are **not** wired by the +Three pieces of OPS machinery ship in the repo but are **not** wired by the Stage 2 deploy scripts. - **Hooks:** the scripts in `.claude-config/hooks/` (SessionStart briefing, @@ -566,6 +602,8 @@ If a new piece of OPS setup needs to run on every host: - `.claude-config/bin/memory-init`, `memory-init.ps1`, `memory-index`: auto-memory git-sync and its index. - `.claude-config/bin/skills-vendor.sh`, `SKILLS/VENDORED.tsv`: the skill mounts. - `.claude-config/bin/tests/`: the tests for the above. +- `.github/workflows/gate.yml`: the CI gate (syntax checks, every suite, a fresh-install smoke test with `verify-ops.sh`, a secrets scan); it runs on the template repository only. +- `SKILLS/THIRD_PARTY_NOTICES.md`: the third-party works the skills adapt, and their licenses. - `SKILLS/README.md`: what gets symlinked into `~/.claude/skills/`. - [`Exploitacious/linuxploitacious`](https://github.com/Exploitacious/linuxploitacious): the public Stage 1 repo (dotfiles + host provisioning). diff --git a/README.md b/README.md index fafcd79..473f9c3 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,12 @@ because these defaults decide what Claude Code may do without asking you: bypass mode: Claude Code runs every tool call, including shell commands, file edits and deletes, and network requests, without a permission prompt. Only `deny` rules and hooks still stop a call. OPS's own brakes are two - PreToolUse hooks: a git guard (hook bypass, force-push, history rewrites, - remote branch deletes, and recursive deletes of a few protected roots) and a - secrets guard (literal credentials written into memory and lessons files). - They are narrow guards, not a sandbox: anything else runs. + PreToolUse hooks: a git guard (commit-hook bypass, force-push, history + rewrites, remote branch deletes, recursive deletes of a few protected roots, + a bare `tmux kill-server`, and a push from your OPS copy to an origin it + cannot prove private) and a secrets guard (literal credentials written into + memory and lessons files). They are narrow guards, not a sandbox: anything + else runs. - **`skipDangerousModePermissionPrompt: true`.** Claude Code normally shows a one-time confirmation before a session enters bypass mode; this skips it, so nothing warns you at launch. @@ -93,6 +95,27 @@ your linuxploitacious checkout, so a local edit (including a `/config` or becomes your memory and your identity: it will hold your context files, your project map, your auto-memory. Set it up correctly the first time. +### Before you start + +You need `bash` (the deployer, hooks, and tests are bash; Windows runs them in +Git Bash), and these on the machine: + +- **`git`, with an identity set.** Run `git config --global user.name "Your + Name"` and `git config --global user.email you@example.com`. A fresh account + often has none, and then every commit fails: the memory sync's, the daily + backup's, and every agent's. Deploy asks `git var` for the author and + committer identity and, when either is missing, prints those two commands and + finishes with a failure named `git-identity` (`DEPLOYMENT.md`, step 7). +- **`gh`, signed in (`gh auth login`).** The private-origin check and branch + cleanup ask GitHub through it. Without a signed-in `gh`, the automatic pushes + stay local and branch cleanup removes nothing. +- **`python3` and `jq`.** Deploy seeds the trust anchor with Python, and the + hooks and the drift gate read JSON with `jq`. +- **Claude Code.** Deploy only looks for the CLI to retire two old plugins; if + it is missing, deploy warns and goes on. +- **Optional: `bubblewrap` (`bwrap`).** Only the destructive-tool test suites + need it (`CONTRIBUTING.md`). + ### 1. Create your OWN PRIVATE repo from this template Use GitHub's **"Use this template"** button, or the CLI: @@ -130,6 +153,31 @@ Two paths, pick one: Both paths are idempotent. Full procedure, both stages, both OSes: see [`DEPLOYMENT.md`](DEPLOYMENT.md). +**On a T3 Code host.** T3 Code runs Claude Code as a provider thread, with no +shell function in the launch, and OPS's boot does not need one: deploy links +the core into `~/.claude/rules/`, which Claude Code loads for a T3 thread as it +does in a terminal (`DEPLOYMENT.md`, "How the core loads"). Two steps are +specific to T3: + +1. In T3 Code, add `~/OPS` as a project and open a thread on it. The first + thread finds no bootstrap marker and runs `BOOTSTRAP.md` (step 3). +2. Run T3's browser setup once per host. T3's page previews and browser tools + run a headless Chrome that needs system libraries and, on Ubuntu 23.10 and + later, an AppArmor profile: + + ```bash + t3="$(ls -1d ~/.t3/runtime/versions/*/t3 | sort -V | tail -1)" + sudo "$t3" browser setup + ``` + + Deploy prints a pointer to this command when it finds a T3 runtime and + never runs it, because it needs sudo. BOOTSTRAP Round G offers to run it with + your yes. Run it again after a T3 upgrade if the browser tools stop working. + +T3 sets each thread's permission mode from its own settings, so the terminal +option in BOOTSTRAP Round F does not reach T3 threads; the managed-settings +option does (`DEPLOYMENT.md`, "T3 Code hosts"). + ### 3. Open Claude Code in the repo and let it bootstrap On the first launch in a fresh copy, the gate in `AGENTS.md` (also named in @@ -139,9 +187,150 @@ you work, plus machine reconnaissance. Together they write your identity (`CONTEXT/USER.md`), a starter voice profile (`CONTEXT/voice.md`), your rulings (`CONTEXT/standing-orders.md`), and the work-tracking, team, and tools slots, then drop the marker. The doctrine already ships; bootstrap fills in the *who*, -never the *how*. On a fresh copy, the bootstrap *is* the session. Under T3 Code, -open a thread on the `~/OPS` project and say "run the bootstrap"; T3 Code hosts -also get a one-time browser setup step (`DEPLOYMENT.md` § "T3 Code hosts"). +never the *how*. On a fresh copy, the bootstrap *is* the session, in a +terminal or in a T3 Code thread alike: the gate lives in `AGENTS.md`, which a +session in this repo reads, so you do not have to ask for it. + +## What deploy does + +`deploy.sh` (Linux and macOS) and `deploy.ps1` (Windows) are Stage 2. Both are +idempotent and safe to re-run after any pull. On Linux, in order: + +1. Link `~/.claude/{skills,commands,agents,workflows}` to the matching OPS + directories, backing up any real directory already there. +2. Mount the shared skills (see "Skill mounts"). +3. Link the always-loaded core into `~/.claude/rules/ops-core.md`. +4. Strip the retired fleet's PATH and wrapper lines from your shell rc files + (whole-line matches only; anything you wrote stays). +5. Wire `claude-root-wrapper.sh` into root's shell rc files when passwordless + sudo exists. It adds `--permission-mode auto` when `claude` runs as root + (Claude Code refuses bypass mode as root) and holds a lock so two users + sharing one config directory never run at once. +6. Write the ultracode `claude()` shim to the untracked `~/.zshrc.local` and + `~/.bashrc.local`. +7. Check the git identity, then link each Claude auto-memory directory into + `.claude-memory/` (`memory-init --auto-commit`). +8. Install the daily backup cron entry. +9. Retire the caveman and ponytail plugins through the `claude plugin` CLI. +10. Mark `$HOME` trusted in `~/.claude.json`, so the repos under it raise no + folder-trust prompt. + +Deploy never writes `~/.claude/settings.json`, because Stage 1 owns it. A step +it cannot finish prints a `[XX]` line, the others still run, and the script +exits non-zero. `--no-system` (`-NoSystem` on Windows) skips every write outside +your home directory (root's rc files, the crontab entry, the scheduled task), +for a test install under a throwaway `HOME`. Windows runs a shorter pipeline; +`DEPLOYMENT.md` lists both. + +## The push gate and its limits + +Your copy holds your memory, context, and notes, so two rules keep it off a +public repo. + +- **The unattended pushes** (`memory-init --auto-commit` during deploy, and the + daily backup) run `.claude-config/bin/origin-private-check` first. It exits 0 + only when GitHub, asked through `gh api`, reports every push URL of `origin` + private. A public origin (the `Exploitacious/OPS` template itself, a public + fork, a copy made public by mistake) or one it cannot verify (`gh` missing or + signed out, offline, a host other than github.com) keeps the commit local; + the output or the backup log names the reason and the fix, and the next run + that passes pushes every held commit. On your own git server, which `gh` + cannot ask, confirm the repo is private and run `git -C ~/OPS config + ops.pushVerified true`. That opt-in covers only what cannot be verified and + never overrides a public answer. +- **Agent pushes** meet the same gate. The `git-guard.sh` hook runs it before + an agent's `git push` from an OPS copy and blocks a push it cannot verify, + one to a remote other than `origin`, and one carrying a `-c` setting that + could move it. + +The limits: the hook is a seatbelt for the pushes an agent makes while +following normal instructions, not a sandbox. Exotic wrappers (`setsid`, +`find -exec`, `ssh`, `python -c`), a push inside a script file, and commands +built at run time are not parsed; the header of +`.claude-config/hooks/git-guard.sh` lists them. The real protection is a +private origin. `GIT_GUARD=off ` lifts the hook for one approved +command; a contributor uses it to push a PR branch (`CONTRIBUTING.md`). + +## The daily backup + +Deploy installs a user cron entry that runs `.claude-config/backup/daily-sync.sh` +at 09:00 (on Windows, the `OPS Daily Backup` task runs the same script in Git +Bash). Each run: + +1. commits the day's changes where they stand, so the tree is never stashed or + moved; +2. pulls with `--rebase`, and aborts a failed pull so the local commit and the + files stay exactly as they were; +3. pushes through the private-origin gate above; +4. pulls every project repo under `PROJECTS/` (`sync-check.sh --auto`, which + skips repos with uncommitted work); +5. runs `branch-gc --apply`, which removes local branches that a merged pull + request provably carried and parks their worktrees in a 7-day trash. + +The script always exits 0, so a failure cannot be seen from the exit code. +It logs to `.claude-config/backup/sync.log` instead, and the session briefing's +`Backup:` line repeats the last run's errors and skipped pushes at the next +boot. + +## Skill mounts + +Portable technique skills are mounted, not copied. `SKILLS/VENDORED.tsv` lists +nine from [`Exploitacious/agent-skills`](https://github.com/Exploitacious/agent-skills): +`delegation-sizing`, `briefing-subagents`, `worktree-isolation`, +`verifying-delegated-output`, `dynamic-workflows`, `meta-skill-creator`, +`grill-me`, `tdd`, and `diagnosing-bugs`. Deploy clones that repo once under +`PROJECTS/Exploitacious/agent-skills` (it never pulls an existing clone, so a +re-deploy cannot slip in an unreviewed upstream change), then runs +`.claude-config/bin/skills-vendor.sh sync` to mirror each skill into +`SKILLS//`. Edit a mounted skill at its source repo, never in `SKILLS/`. +`skills-vendor.sh --check` prints `OK`, `DRIFT` (the source changed and the +mirror did not), or `MISSING` (the source is not cloned). The sync never +deletes a folder it did not write: your own skill under a mounted name, or a +hand-edited mirror, moves to `.claude-config/backup/skills-replaced/` first. +Add a row to `VENDORED.tsv` for an org or private source of your own +(`SKILLS/README.md`). Some mounted and local skills adapt third-party work; +[`SKILLS/THIRD_PARTY_NOTICES.md`](SKILLS/THIRD_PARTY_NOTICES.md) lists each +source and its license. + +## Template sync + +A copy made from the template shares no git history with it, so `git merge` has +no common ancestor. The `harness-update` skill ("update the harness") is the +safe path across that gap, and `.claude-config/bin/harness-update-scan.sh` is +its mechanical half. Run from inside your copy (`cd ~/OPS`), the scan adds an +`ops-template` remote, fetches it, and classifies every file as NEW, UPDATE, +CONFLICT, MODE, REMOVED, or IDENTICAL. By default it reports and touches +nothing. `--apply-safe` copies NEW and UPDATE files only, never a CONFLICT, a +MODE change, a deletion, or a commit. Your identity files (`USER.md`, +`voice.md`, `standing-orders.md`, the `work-tracking`, `team`, and `tools` +slots, `.bootstrapped`, `CONTEXT/projects/`), memory, and handoff surfaces are +excluded in both directions, so a refresh never overwrites who you are. The +marker `.claude-config/ops-upstream-ref` records the last upstream commit +pulled, so the next scan shows only what changed since. The template's CI +workflow (`.github/workflows/gate.yml`) arrives like any other file; it runs +only on the template repository, so delete it from your copy if you like. +Copies older than the 2026-10-19 release also migrate by hand once +(`DEPLOYMENT.md`, "Migrating to the core boot"). + +## Checking your copy + +- **The drift gate:** `bash ~/OPS/.claude-config/bin/verify-ops.sh` + machine-checks the conventions that used to live only in prose: the core's + size and link, the model pins, the retired files, the skill mounts, the + sandbox guards, and more. Exit 0 means no FAIL (a WARN is advice). `--quiet` + prints only WARN and FAIL lines, and `OPS_DIR=` checks another + checkout. Its host checks read `$HOME` (the core link, the Stage-1 + settings), so on a machine with no deployed OPS it fails until you deploy. An + optional user timer runs it nightly, and the session briefing shows the + result (`DEPLOYMENT.md`, "Hooks and the drift gate"). +- **The tests:** every script has a suite under `.claude-config/bin/tests/`, a + `*-selftest` beside its subject, or a hook self-test. A suite that runs a + tool that deletes or moves files runs only inside `sandbox-run`. + `CONTRIBUTING.md` has the commands for all of them. +- **CI:** `.github/workflows/gate.yml` runs the syntax checks, the suites, a + fresh-install smoke test followed by `verify-ops.sh`, and a secrets scan on + every push and pull request to the template repository, with no secrets, so + a fork's pull request gets the same run. ## What's inside @@ -152,7 +341,8 @@ OPS/ ├── DEPLOYMENT.md # the two-stage deploy (linuxploitacious -> deploy.sh), migration, T3 hosts ├── BOOTSTRAP.md # first-launch interview + machine recon (writes your identity files) ├── LICENSE # MIT -├── CONTRIBUTING.md # the porting discipline: how private-harness improvements land here +├── CONTRIBUTING.md # the porting discipline and the gates: how private-harness improvements land here +├── .github/workflows/ # gate.yml: the CI gate (runs on the template repository only) ├── CONTEXT/ # core.md always loaded; everything else on demand │ ├── core.md # the always-loaded rules (linked into ~/.claude/rules/ops-core.md) │ ├── how-i-work.md # principles P1-P15 + situational rules (the why behind core.md) @@ -162,7 +352,7 @@ OPS/ │ ├── USER.md / voice.md / standing-orders.md # your identity, voice, rulings: templates BOOTSTRAP fills, never synced │ ├── work-tracking.md / team.md / tools.md / doctrine.md # the context slots │ └── projects/ # per-project lessons, loaded only when on that project -├── SKILLS/ # Claude Code Skills + GUI Project twins (symlinked -> ~/.claude/skills/) +├── SKILLS/ # Claude Code Skills + GUI Project twins (symlinked -> ~/.claude/skills/); THIRD_PARTY_NOTICES.md ├── .claude-config/ # Stage 2 deploy + the machinery it does not itself install │ ├── deploy.sh / deploy.ps1 # Stage 2 deployer (links, core rule, skill mounts, memory sync, ...) │ ├── hooks/ # SessionStart briefing, pre-compact snapshot + branch-gc, context-watch, secrets/git guards @@ -234,4 +424,6 @@ layer for the Claude Code CLI, not a desktop product. ## License -MIT; see [`LICENSE`](LICENSE). +MIT; see [`LICENSE`](LICENSE). Skills adapted from third-party work keep their +upstream licenses; see +[`SKILLS/THIRD_PARTY_NOTICES.md`](SKILLS/THIRD_PARTY_NOTICES.md). From 394f3329189716c4fade99e8338b007ec77faebf Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:44:27 -0400 Subject: [PATCH 05/19] docs(changelog): one 2026-10-19 release entry The two port releases each added a 2026-10-19 entry (about 320 lines for the core boot and the fleet retirement, 70 for the hooks, sandbox and review split), so a reader met the same day twice with no overview. They are now one entry with an intro that says what the release is and why, and subsections: boot and doctrine, hooks and compaction, the private-origin push gate, backups, skill mounts, template sync, deploy and the drift gate, landing and review, the sandbox, and fixes from review. A final subsection records this change: the em dash sweep, the doc corrections, the third-party notices and the CI gate. All 51 bullets are kept word for word, so no fact is lost. The only edits are the lead-ins of the two bullets that were both titled "Review hardening", which now say what each hardened. The 2026-10-18 entry and everything below it are untouched. --- CHANGELOG.md | 487 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 284 insertions(+), 203 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 758ee1f..642ee68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,84 +4,23 @@ Notable changes to OPS, newest first. Format: the date, then what changed and wh matters. This file starts fresh at the public release; the harness's private prehistory is deliberately not part of it. -## 2026-10-19: lane-aware hooks, branch cleanup, the sandbox, and the review split - -The hooks assumed one 1M window for every caller and still told sessions to -self-compact through a tmux typer that cannot exist under T3 Code. OPS also had -no rule on where destructive tests run and no landing tooling. This release -ports those pieces from the maintainer's private harness. - -- **Lanes are sized by their own model.** `context-watch.sh` treats a hook call - carrying `agent_id` as a sub-agent lane: it reads the lane's own transcript, - sizes the window from the lane's model (1M for the current Opus, Sonnet and - Fable ids, 200K for the 4.6 and Haiku 4.5 ids, one 95K backstop under Haiku - 5.5's 100K ceiling), and speaks only mid-turn, a reminder at 60% and a - wrap-up at 80%. Before, every lane got the main session's ladder read from - the main transcript, so a lane of a busy foreman was told to compact at once. - The agent definitions carry the lane return contract. -- **No session self-compacts.** `bin/compact-cycle.sh` is deleted: it typed - `/compact` into a tmux pane and could not tell a finished compaction from a - failed one. Every nag now routes to `pre-compact-synthesis` and then the - Operator's `/compact`; sessions in tmux or herdr are run by hand. -- **`branch-gc`** removes local branches a merged PR provably carried and - parks their worktrees in a 7-day trash it can restore from; it fails closed - on any doubt. It runs nightly from `daily-sync.sh`, from the PreCompact hook - for the cwd repo (detached; skipped when the payload names a sub-agent), - and at closeout. -- **Destructive tests run in a sandbox.** `bin/sandbox-run` (bubblewrap: a - read-only root, a fresh home and `/tmp`, nothing writable by default) plus - a guard every suite that runs branch-gc, pre-compact, memory-init, deploy or - skills-vendor now starts with; outside the sandbox those suites exit 97. - `verify-ops.sh` check 24 fails an unguarded one. Why: in the private harness - an unsandboxed mutation sweep wiped most of a home directory. -- **The review split.** The open-ended `ops-reviewer` lane is retired. Code - where a miss is expensive goes to a second vendor's model - (`bin/second-vendor-review` is the optional hand-off), or with none to an - Opus 5.5 refutation lane plus the Operator's own read; other code gets a - Sonnet 5.5 refutation lane; docs and decisions land on verification. - `how-i-work.md` carries the refutation brief shape. -- **Backups that fail are visible.** The session briefing's `Backup:` line - repeats the last nightly run's OPS-stage errors and skipped pushes, since - `daily-sync.sh` exits 0 and nothing read its log. An origin with no branch - of that name yet is now a first push; before, its pull failed every night - and nothing was ever backed up. -- **Fixes:** every git-guard check now sees through an `rtk` prefix (from a - project repo, `rtk git push --force`, `rtk git branch -D` and the other - rtk forms of a blocked command all passed); `harness-update-scan.sh` exits 7 - when git cannot list the delta, instead of reporting a clean empty one; - deploy's failure summary mentions the core only when the core rule failed; - the pre-compact-synthesis skill states the push gate's verdict rule; - `verify-ops.sh` check 25 warns on a memory link whose pool is gone. -- **`guard-selftest.sh` counts every failure.** Its rtk probes read their - JSON through a pipe, which ran each probe in a subshell: a failing rtk guard - printed FAIL and the selftest still exited 0 with "ALL GUARDS ENFORCE", the - exit code `deploy.ps1` acts on. They read a here-string now, and - `tests/guard-selftest-test.sh` inverts one guard verdict per probe site and - checks the exit code and the count. -- **`second-vendor-review` never runs a reviewer without its cap.** With no - `timeout` or `gtimeout` on PATH (stock macOS) it skipped `--timeout` in - silence, so a hung reviewer blocked forever and a slow one was written up as - a review. It now exits 3 before any git work and names the fix - (`brew install coreutils`); `gtimeout` alone is enough. -- **Ported:** `bin/transcript-usage.py` (token profile from transcripts) with - a test. -- **Review hardening.** `sandbox-run` passes only an allowlisted set of open - file descriptors into bubblewrap: an inherited descriptor (fd 255 included) - let a suite write to the host behind the read-only mounts. `second-vendor-review` - writes its packet and result to private, unique paths published atomically, - stops when the digest cannot be read, and no longer records the reviewer - command, whose inline assignments could carry a credential. New - `sandbox-run-fd-test.py`, which runs outside the sandbox like `sandbox-run-test.sh`. - -## 2026-10-19: one always-loaded core, and the fleet retired +## 2026-10-19: one always-loaded core, private-only pushes, lane-aware hooks, the sandbox, and a CI gate OPS booted through a foreman charter and an identity digest that a `claude()` shell function appended to the system prompt. Only launches typed into an interactive shell ran that function, so a T3 Code thread, an IDE session, or -`claude -p` started with none of the doctrine. This release replaces that boot +`claude -p` started with none of the doctrine. The hooks assumed one 1M window +for every caller and still told sessions to self-compact through a tmux typer +that cannot exist under T3 Code. OPS also had no rule on where destructive +tests run, no landing tooling, and no CI. This release replaces the boot with one always-loaded file, moves the identity layer to files the template -sync never touches, and retires the multi-agent fleet. Existing copies migrate -by hand once: `DEPLOYMENT.md` § "Migrating to the core boot". +sync never touches, retires the multi-agent fleet, keeps every automatic push +on a private origin, ports the lane-aware hooks, branch cleanup, sandbox and +review split from the maintainer's private harness, and closes with a docs +pass, third-party notices and a CI gate. Existing copies migrate by hand once: +`DEPLOYMENT.md` § "Migrating to the core boot". + +### Boot and doctrine - **`CONTEXT/core.md` loads on every launch path.** One generic file, under 15,360 bytes, with the rules every session and lane needs on every turn: @@ -109,10 +48,6 @@ by hand once: `DEPLOYMENT.md` § "Migrating to the core boot". `start-remote-claude.sh`, and the `ops-memory-gc` timer. The `claude()` shim is ultracode-only, and a re-deploy heals the old boot shim while keeping a Round F permission edit. -- **Template sync.** `harness-update-scan.sh` excludes `USER.md`, `voice.md`, - `standing-orders.md`, the `work-tracking`, `team`, and `tools` slots, and the - marker, plus the retired identity names, so a delta scan never lists an - unmigrated identity file as REMOVED. This landed first, ahead of the renames. - **BOOTSTRAP** writes the new set: `USER.md`, a `voice.md` starter, and a dated standing order for every Operator choice. Round F keeps its text and records into `standing-orders.md`; the new Round G sets up T3 Code's headless @@ -126,31 +61,26 @@ by hand once: `DEPLOYMENT.md` § "Migrating to the core boot". `claude-root-wrapper.sh`, which deploy wires for root only. The wrapper's conflict hint uses `pgrep -l`, because `pgrep -af` prints other sessions' full command lines. -- **Mounted skills arrive on a fresh install.** Deploy clones the public skill - source once and runs `skills-vendor.sh sync`; it never pulls an existing - clone. `skills-vendor.sh` honors `OPS_DIR`, compares file content, and falls - back to tar where rsync is missing, as on a stock Debian container. -- **Deploy** strips the fleet's PATH and wrapper lines, takes `--no-system` - (`-NoSystem` on Windows) to skip writes outside `$HOME` for test installs, - and exits non-zero when a step fails. -- **The drift gate** checks the new boot: 16 fails when the core is missing, - over budget, or not linked; 1b fails on a root `CLAUDE.md`; 5 counts the - principles in `how-i-work.md`; 17 and 21b warn on retired hooks and files; - 21 is the `USER.md` canary; 23 requires every identity exclusion. - `verify-ops.sh` checks another checkout through `OPS_DIR`. New tests: - `.claude-config/bin/tests/` (memory-index, root wrapper, deploy rc and core - wiring, skills-vendor) and new `verify-ops-selftest.sh` cases. -- **Skills refreshed.** `grill-me`, `tdd` and `diagnosing-bugs` mount from - the public agent-skills library alongside the six earlier mounts; deploy - clones it and runs `skills-vendor.sh sync`. `harness-update`, - `pre-compact-synthesis`, `session-close`, `session-handoff`, `memory-prune`, - `operator-voice` and the grabit skills follow the current rest-stop and close - rules: a session closes only on the Operator's word, and the close stamp is - now cleared (its redirect was missing, so the `rm` became a `printf` - argument). New `scripts/inventory` for memory-prune, which exits 1 on a - missing store so a wrong path cannot read as a clean pool. New generic - `operating-model` and `stewardship` skills. The `fleet-doctrine` skill is - removed with the fleet. + +### Hooks and compaction + +- **Lanes are sized by their own model.** `context-watch.sh` treats a hook call + carrying `agent_id` as a sub-agent lane: it reads the lane's own transcript, + sizes the window from the lane's model (1M for the current Opus, Sonnet and + Fable ids, 200K for the 4.6 and Haiku 4.5 ids, one 95K backstop under Haiku + 5.5's 100K ceiling), and speaks only mid-turn, a reminder at 60% and a + wrap-up at 80%. Before, every lane got the main session's ladder read from + the main transcript, so a lane of a busy foreman was told to compact at once. + The agent definitions carry the lane return contract. +- **No session self-compacts.** `bin/compact-cycle.sh` is deleted: it typed + `/compact` into a tmux pane and could not tell a finished compaction from a + failed one. Every nag now routes to `pre-compact-synthesis` and then the + Operator's `/compact`; sessions in tmux or herdr are run by hand. + +### Pushes stay on a private origin + +Your copy holds your memory and notes, so every push of it meets one gate. + - **Automatic pushes go only to a private origin.** `memory-init --auto-commit` and the daily backup (`daily-sync.sh`, which the Windows task also runs) pushed to wherever origin pointed, so a tryout clone of this @@ -165,49 +95,6 @@ by hand once: `DEPLOYMENT.md` § "Migrating to the core boot". config ops.pushVerified true`, which never overrides a public answer. Tests: `origin-private-check-test.sh`, `daily-sync-test.sh`, and new memory-init cases in bash and PowerShell. -- **Skill mounts never delete a folder they did not write.** A manifest row - replaced whatever sat at `SKILLS/`, so this release's `grill-me`, - `tdd`, and `diagnosing-bugs` rows would have deleted an existing copy's own - skill of that name. `skills-vendor.sh` now records a digest of each mirror - it writes (`SKILLS/.vendored-state.tsv`) and moves any other destination, - including a hand-edited mirror, to - `.claude-config/backup/skills-replaced/-/` before installing, - with a `BACKUP` line naming the path; a failed install moves it back. - Mirrors already equal to their source update in place as before. -- **Template sync for older copies.** `harness-update-scan.sh` is self-contained - by contract (it finds its root from the working directory and loads no helper - file), so a copy that predates it runs it straight from the template; the - harness-update skill gains "Updating a copy that predates this skill" with the - commands. The template's history was rewritten on 2026-10-19 to remove a - private name from one old commit, so a `last_synced` marker written before - then names a commit the template no longer has: the scan now translates it to - its rewritten twin (same tree) and stays in delta mode instead of falling back - to a first-sync compare. New `harness-update-scan-test.sh`. -- **Agents push memory and batons only to a private origin.** The - pre-compact-synthesis and session-handoff skills run `origin-private-check` - before their additive-sync push, the same gate `memory-init` and the daily - backup use. -- **Backups that cannot happen say so.** The daily backup checks its commit: - a failed commit (no git identity, a hook) or a failed stash is logged as an - ERROR with the fix, and nothing is pushed or reported as backed up; before, - the log claimed a backup that did not exist. A new memory pool gets a - `.gitkeep`, because the backup's `git stash -u` dropped the empty directory - and left the memory link dangling; `memory-init` also recreates a pool an - earlier backup removed, and commits only the pool path. A failed crontab - install now fails the deploy. -- **Template sync delivers renamed and executable files.** - `harness-update-scan.sh` read `git diff --name-status` with git's default - rename detection, so a renamed file came back as one line holding two paths - and dropped out of the delta: never NEW, its old path never REMOVED, and the - next sync commit moved `last_synced` past it for good (against the public - main of 2026-10-19 that lost `memory-init` and four more files). The delta - now runs with `--no-renames`, so a rename is a REMOVED plus a NEW. - `--apply-safe` sets each file's mode from the upstream tree entry: NEW files - were written at the umask mode, so `memory-init` arrived non-executable and - the migration's re-deploy failed. A marker whose last line has no trailing - newline is read again; before, its `last_synced` was dropped and the scan - fell back to a first-sync compare without a warning. New cases in - `harness-update-scan-test.sh`. - **The push gate needs its own verdict, not just exit 0.** `daily-sync.sh` and `memory-init` counted exit 0 from `bash origin-private-check` as verified, and an empty file or one cut off after its function definitions @@ -224,6 +111,10 @@ by hand once: `DEPLOYMENT.md` § "Migrating to the core boot". case, as the PowerShell twin does. New cases in `origin-private-check-test.sh`, `daily-sync-test.sh`, `memory-init-test.sh` and `memory-init-windows-test.ps1`. +- **Agents push memory and batons only to a private origin.** The + pre-compact-synthesis and session-handoff skills run `origin-private-check` + before their additive-sync push, the same gate `memory-init` and the daily + backup use. - **Agent pushes of the OPS copy go through the push gate.** The skills and `AGENTS.md` told agents to commit and push batons, the memory mirror and trunk work, and only prose asked them to run `origin-private-check` first, @@ -237,38 +128,6 @@ by hand once: `DEPLOYMENT.md` § "Migrating to the core boot". Without Python 3 it checks every directory the command names instead. The session-handoff, session-close and self-compact docs, the handoff README, `AGENTS.md` and `core.md` now say so. New `guard-selftest.sh` cases. -- **Skill mounts keep every folder they cannot prove they wrote, and swap - mirrors in whole.** `skills-vendor.sh` treated an unrecorded folder under - one of the six names that mounted before the record existed as its own - mirror, so a user's own or hand-edited `meta-skill-creator` (or any of the - six) was overwritten in place with no backup. An unrecorded folder that - differs from its source is now moved aside like any other; one equal to its - source still updates in place. The rsync engine synced straight into the - mirror, so a failed or interrupted rsync left it half-updated, and the next - sync then moved it aside with its GUI-local files; both engines now build - the mirror in a temp dir and swap it in, keeping the old one on any - failure. The `BACKUP` line is printed as soon as a folder is moved aside, - so a later failure still says where it went. New cases in - `skills-vendor-test.sh`. -- **A failed backup pull puts the work back.** When `git pull --rebase` - failed (offline, no credentials under cron), `daily-sync.sh` returned with - the Operator's uncommitted and untracked work still in its stash, so the - files vanished from the tree, the mounted skills went missing, and later - runs saw a clean tree and never restored them. It now aborts a rebase the - pull left half-done, pops the stash with its index so the tree is exactly - as before, logs the failure as an ERROR with git's reason, and commits and - pushes nothing that run; a pop that conflicts still names the stash. New - cases in `daily-sync-test.sh`. -- **Deploy checks the git identity.** A fresh Debian user has no git - identity (an empty GECOS name, no FQDN for an email), so memory-init's - commit, the daily backup and every agent commit failed while `deploy.sh` - printed "Deploy complete." `deploy.sh` and `deploy.ps1` now ask `git var` - for the author and committer identity before memory-init runs and, when - either is missing, print the two `git config --global` commands and fail - the deploy (`git-identity`). `memory-init` and `memory-init.ps1` show - git's own stderr for a failed commit instead of hiding it behind a - "(hook/signing?)" guess. New cases in `memory-init-test.sh` and - `memory-init-windows-test.ps1`. - **A redirect after a push is not its remote.** The push gate read the first word after `git push` as the remote, so `git push 2>&1`, `1>&2`, `>&2`, `&>/dev/null` and the like were blocked on a verified-private copy @@ -317,31 +176,9 @@ by hand once: `DEPLOYMENT.md` § "Migrating to the core boot". `gh` 30 s, the same 30 s the harness gives the git-guard hook, so a hung `gh` timed the hook out before the agent saw why. `gh` now gets 10 s. New case in `origin-private-check-test.sh`. -- **Template sync writes only into an OPS copy.** `harness-update-scan.sh` - scans the repo of the working directory, and the harness-update skill ran - it by absolute path, so from a project clone it added the `ops-template` - remote there and `--apply-safe` copied the whole template into it. It now - refuses (`ERR_NOREPO`, nothing written) unless the repo has `BOOTSTRAP.md` - or `.claude-config/`, and prints the `cd` to run from; the skill's - commands run as `(cd ~/OPS && ...)`. New case in - `harness-update-scan-test.sh`. -- **Template sync sees modes.** Files were compared by blob alone, so an - upstream `chmod +x` with the same content was never applied, and a script - an earlier scan wrote without its exec bit (which then failed deploy's - `memory-init` check) read as IDENTICAL forever. Files now compare as (mode, - blob): a mode-only change is an UPDATE when the local mode is still the - last-synced one, and any other exec-bit difference is a new MODE line, with - the `chmod` to run, in delta and first-sync scans alike. New cases. -- **Template sync sets modes under any umask.** `chmod +x`/`-x` with no - `who` is filtered by the umask, so under 027 clearing the bit left group - or other bits set and the scan exited 6 after writing the file. It now - uses `chmod a+x`/`a-x`. New case under umask 027. -- **Template symlinks arrive as symlinks.** A mode 120000 entry was written - as a regular file holding the link target. It is now created with - `ln -s`. Each applied file is built beside its destination and renamed - into place, so a local symlink is replaced rather than written through, - and the scan can update itself while it runs. A local path that cannot be - hashed is a CONFLICT, never NEW. New case. + +### Backups + - **The daily backup never moves the Operator's work.** `daily-sync.sh` stashed the tree, pulled, then popped: a run killed between stash and pop (a cron timeout, a reboot) left the work in a stash that later runs never @@ -356,6 +193,68 @@ by hand once: `DEPLOYMENT.md` § "Migrating to the core boot". `daily-sync-test.sh`: the tree byte for byte (modes and empty directories) after an unreachable origin, a conflicting rebase and a killed run, no stash ever created, and modes kept through a rebase. +- **Backups that cannot happen say so.** The daily backup checks its commit: + a failed commit (no git identity, a hook) or a failed stash is logged as an + ERROR with the fix, and nothing is pushed or reported as backed up; before, + the log claimed a backup that did not exist. A new memory pool gets a + `.gitkeep`, because the backup's `git stash -u` dropped the empty directory + and left the memory link dangling; `memory-init` also recreates a pool an + earlier backup removed, and commits only the pool path. A failed crontab + install now fails the deploy. +- **A failed backup pull puts the work back.** When `git pull --rebase` + failed (offline, no credentials under cron), `daily-sync.sh` returned with + the Operator's uncommitted and untracked work still in its stash, so the + files vanished from the tree, the mounted skills went missing, and later + runs saw a clean tree and never restored them. It now aborts a rebase the + pull left half-done, pops the stash with its index so the tree is exactly + as before, logs the failure as an ERROR with git's reason, and commits and + pushes nothing that run; a pop that conflicts still names the stash. New + cases in `daily-sync-test.sh`. +- **Backups that fail are visible.** The session briefing's `Backup:` line + repeats the last nightly run's OPS-stage errors and skipped pushes, since + `daily-sync.sh` exits 0 and nothing read its log. An origin with no branch + of that name yet is now a first push; before, its pull failed every night + and nothing was ever backed up. + +### Skills and mounts + +- **Mounted skills arrive on a fresh install.** Deploy clones the public skill + source once and runs `skills-vendor.sh sync`; it never pulls an existing + clone. `skills-vendor.sh` honors `OPS_DIR`, compares file content, and falls + back to tar where rsync is missing, as on a stock Debian container. +- **Skills refreshed.** `grill-me`, `tdd` and `diagnosing-bugs` mount from + the public agent-skills library alongside the six earlier mounts; deploy + clones it and runs `skills-vendor.sh sync`. `harness-update`, + `pre-compact-synthesis`, `session-close`, `session-handoff`, `memory-prune`, + `operator-voice` and the grabit skills follow the current rest-stop and close + rules: a session closes only on the Operator's word, and the close stamp is + now cleared (its redirect was missing, so the `rm` became a `printf` + argument). New `scripts/inventory` for memory-prune, which exits 1 on a + missing store so a wrong path cannot read as a clean pool. New generic + `operating-model` and `stewardship` skills. The `fleet-doctrine` skill is + removed with the fleet. +- **Skill mounts never delete a folder they did not write.** A manifest row + replaced whatever sat at `SKILLS/`, so this release's `grill-me`, + `tdd`, and `diagnosing-bugs` rows would have deleted an existing copy's own + skill of that name. `skills-vendor.sh` now records a digest of each mirror + it writes (`SKILLS/.vendored-state.tsv`) and moves any other destination, + including a hand-edited mirror, to + `.claude-config/backup/skills-replaced/-/` before installing, + with a `BACKUP` line naming the path; a failed install moves it back. + Mirrors already equal to their source update in place as before. +- **Skill mounts keep every folder they cannot prove they wrote, and swap + mirrors in whole.** `skills-vendor.sh` treated an unrecorded folder under + one of the six names that mounted before the record existed as its own + mirror, so a user's own or hand-edited `meta-skill-creator` (or any of the + six) was overwritten in place with no backup. An unrecorded folder that + differs from its source is now moved aside like any other; one equal to its + source still updates in place. The rsync engine synced straight into the + mirror, so a failed or interrupted rsync left it half-updated, and the next + sync then moved it aside with its GUI-local files; both engines now build + the mirror in a temp dir and swap it in, keeping the old one on any + failure. The `BACKUP` line is printed as soon as a folder is moved aside, + so a later failure still says where it went. New cases in + `skills-vendor-test.sh`. - **Skill mounts swap on macOS.** `skills-vendor.sh` swapped each mirror in with `mv -T`, which only GNU mv has, so on macOS and BSD every swap failed and even a fresh install exited 3. Both renames now target a name that does @@ -384,7 +283,137 @@ by hand once: `DEPLOYMENT.md` § "Migrating to the core boot". engine ignores time-only itemize lines when deciding UNCHANGED, so a mirror whose directory time moved no longer flips between SYNCED and UNCHANGED; a mode or content change still syncs. -- **Review hardening.** git-guard closes push bypasses through config + +### Template sync + +- **Template sync.** `harness-update-scan.sh` excludes `USER.md`, `voice.md`, + `standing-orders.md`, the `work-tracking`, `team`, and `tools` slots, and the + marker, plus the retired identity names, so a delta scan never lists an + unmigrated identity file as REMOVED. This landed first, ahead of the renames. +- **Template sync for older copies.** `harness-update-scan.sh` is self-contained + by contract (it finds its root from the working directory and loads no helper + file), so a copy that predates it runs it straight from the template; the + harness-update skill gains "Updating a copy that predates this skill" with the + commands. The template's history was rewritten on 2026-10-19 to remove a + private name from one old commit, so a `last_synced` marker written before + then names a commit the template no longer has: the scan now translates it to + its rewritten twin (same tree) and stays in delta mode instead of falling back + to a first-sync compare. New `harness-update-scan-test.sh`. +- **Template sync delivers renamed and executable files.** + `harness-update-scan.sh` read `git diff --name-status` with git's default + rename detection, so a renamed file came back as one line holding two paths + and dropped out of the delta: never NEW, its old path never REMOVED, and the + next sync commit moved `last_synced` past it for good (against the public + main of 2026-10-19 that lost `memory-init` and four more files). The delta + now runs with `--no-renames`, so a rename is a REMOVED plus a NEW. + `--apply-safe` sets each file's mode from the upstream tree entry: NEW files + were written at the umask mode, so `memory-init` arrived non-executable and + the migration's re-deploy failed. A marker whose last line has no trailing + newline is read again; before, its `last_synced` was dropped and the scan + fell back to a first-sync compare without a warning. New cases in + `harness-update-scan-test.sh`. +- **Template sync writes only into an OPS copy.** `harness-update-scan.sh` + scans the repo of the working directory, and the harness-update skill ran + it by absolute path, so from a project clone it added the `ops-template` + remote there and `--apply-safe` copied the whole template into it. It now + refuses (`ERR_NOREPO`, nothing written) unless the repo has `BOOTSTRAP.md` + or `.claude-config/`, and prints the `cd` to run from; the skill's + commands run as `(cd ~/OPS && ...)`. New case in + `harness-update-scan-test.sh`. +- **Template sync sees modes.** Files were compared by blob alone, so an + upstream `chmod +x` with the same content was never applied, and a script + an earlier scan wrote without its exec bit (which then failed deploy's + `memory-init` check) read as IDENTICAL forever. Files now compare as (mode, + blob): a mode-only change is an UPDATE when the local mode is still the + last-synced one, and any other exec-bit difference is a new MODE line, with + the `chmod` to run, in delta and first-sync scans alike. New cases. +- **Template sync sets modes under any umask.** `chmod +x`/`-x` with no + `who` is filtered by the umask, so under 027 clearing the bit left group + or other bits set and the scan exited 6 after writing the file. It now + uses `chmod a+x`/`a-x`. New case under umask 027. +- **Template symlinks arrive as symlinks.** A mode 120000 entry was written + as a regular file holding the link target. It is now created with + `ln -s`. Each applied file is built beside its destination and renamed + into place, so a local symlink is replaced rather than written through, + and the scan can update itself while it runs. A local path that cannot be + hashed is a CONFLICT, never NEW. New case. + +### Deploy and the drift gate + +- **Deploy** strips the fleet's PATH and wrapper lines, takes `--no-system` + (`-NoSystem` on Windows) to skip writes outside `$HOME` for test installs, + and exits non-zero when a step fails. +- **Deploy checks the git identity.** A fresh Debian user has no git + identity (an empty GECOS name, no FQDN for an email), so memory-init's + commit, the daily backup and every agent commit failed while `deploy.sh` + printed "Deploy complete." `deploy.sh` and `deploy.ps1` now ask `git var` + for the author and committer identity before memory-init runs and, when + either is missing, print the two `git config --global` commands and fail + the deploy (`git-identity`). `memory-init` and `memory-init.ps1` show + git's own stderr for a failed commit instead of hiding it behind a + "(hook/signing?)" guess. New cases in `memory-init-test.sh` and + `memory-init-windows-test.ps1`. +- **The drift gate** checks the new boot: 16 fails when the core is missing, + over budget, or not linked; 1b fails on a root `CLAUDE.md`; 5 counts the + principles in `how-i-work.md`; 17 and 21b warn on retired hooks and files; + 21 is the `USER.md` canary; 23 requires every identity exclusion. + `verify-ops.sh` checks another checkout through `OPS_DIR`. New tests: + `.claude-config/bin/tests/` (memory-index, root wrapper, deploy rc and core + wiring, skills-vendor) and new `verify-ops-selftest.sh` cases. + +### Landing and review + +- **`branch-gc`** removes local branches a merged PR provably carried and + parks their worktrees in a 7-day trash it can restore from; it fails closed + on any doubt. It runs nightly from `daily-sync.sh`, from the PreCompact hook + for the cwd repo (detached; skipped when the payload names a sub-agent), + and at closeout. +- **The review split.** The open-ended `ops-reviewer` lane is retired. Code + where a miss is expensive goes to a second vendor's model + (`bin/second-vendor-review` is the optional hand-off), or with none to an + Opus 5.5 refutation lane plus the Operator's own read; other code gets a + Sonnet 5.5 refutation lane; docs and decisions land on verification. + `how-i-work.md` carries the refutation brief shape. +- **`second-vendor-review` never runs a reviewer without its cap.** With no + `timeout` or `gtimeout` on PATH (stock macOS) it skipped `--timeout` in + silence, so a hung reviewer blocked forever and a slow one was written up as + a review. It now exits 3 before any git work and names the fix + (`brew install coreutils`); `gtimeout` alone is enough. +- **Ported:** `bin/transcript-usage.py` (token profile from transcripts) with + a test. + +### Destructive tests and the sandbox + +- **Destructive tests run in a sandbox.** `bin/sandbox-run` (bubblewrap: a + read-only root, a fresh home and `/tmp`, nothing writable by default) plus + a guard every suite that runs branch-gc, pre-compact, memory-init, deploy or + skills-vendor now starts with; outside the sandbox those suites exit 97. + `verify-ops.sh` check 24 fails an unguarded one. Why: in the private harness + an unsandboxed mutation sweep wiped most of a home directory. +- **`guard-selftest.sh` counts every failure.** Its rtk probes read their + JSON through a pipe, which ran each probe in a subshell: a failing rtk guard + printed FAIL and the selftest still exited 0 with "ALL GUARDS ENFORCE", the + exit code `deploy.ps1` acts on. They read a here-string now, and + `tests/guard-selftest-test.sh` inverts one guard verdict per probe site and + checks the exit code and the count. + +### Fixes and hardening from review + +- **Fixes:** every git-guard check now sees through an `rtk` prefix (from a + project repo, `rtk git push --force`, `rtk git branch -D` and the other + rtk forms of a blocked command all passed); `harness-update-scan.sh` exits 7 + when git cannot list the delta, instead of reporting a clean empty one; + deploy's failure summary mentions the core only when the core rule failed; + the pre-compact-synthesis skill states the push gate's verdict rule; + `verify-ops.sh` check 25 warns on a memory link whose pool is gone. +- **Review hardening of the sandbox and the reviewer hand-off.** `sandbox-run` passes only an allowlisted set of open + file descriptors into bubblewrap: an inherited descriptor (fd 255 included) + let a suite write to the host behind the read-only mounts. `second-vendor-review` + writes its packet and result to private, unique paths published atomically, + stops when the digest cannot be read, and no longer records the reviewer + command, whose inline assignments could carry a credential. New + `sandbox-run-fd-test.py`, which runs outside the sandbox like `sandbox-run-test.sh`. +- **Review hardening of the push gate, template sync, skill sync and backup.** git-guard closes push bypasses through config includes, inherited alias settings and alias options. The template scanner refuses any path whose parent is a symlink, so it cannot write outside the copy or onto an excluded identity file. Skill sync serializes concurrent @@ -392,6 +421,58 @@ by hand once: `DEPLOYMENT.md` § "Migrating to the core boot". instead of deleting it. The daily backup keeps every tracked file's permissions across a pull. +### Docs, notices and CI + +- **No em dashes.** The 139 em dashes still in tracked text (16 files: the + `CONTEXT/` templates and the project kata, comments in hooks, scripts and + self-tests, the design brand template, and two READMEs) are rewritten with + colons, commas, semicolons or parentheses; no en dashes were present. Code + changed only in comments and printed text (the handoff banner, the + self-tests' result lines, `sync-check.sh`'s messages, the `ops-verify` unit + description), and no test greps any of those strings. +- **The docs match the code.** README gains the requirements (git with an + identity, a signed-in `gh`, `python3`, `jq`), the T3 Code path, and sections + on what deploy does, the push gate and its limits, the daily backup, skill + mounts, template sync, and how to check a copy; its list of what the git + guard blocks now includes a bare `tmux kill-server` and an unverified push. + DEPLOYMENT said the trust anchor lives in `~/.claude/.claude.json`; deploy + writes `~/.claude.json`, beside the directory. It also called a failed skill + clone a warning, though that fails the deploy (`skills-mount`). It gains the + exact outputs of its verification commands, a recipe for testing a deploy + under a throwaway `HOME`, and a corrected count of the machinery deploy does + not wire. CONTRIBUTING gains the private-term rule (your list of private + terms never goes in a file, a commit message or a PR), a section on where + destructive tests run and why, and the `GIT_GUARD=off` rule for a PR branch. + Its suite loop printed `FAILED` but always exited 0; the command now counts + failures and exits non-zero, and runs the self-tests and the PowerShell + suites too. It also no longer implies that `verify-ops.sh` on a bare checkout + means anything: the gate reads host state from `$HOME`, so a machine with no + deployed OPS needs the throwaway-`HOME` run. AGENTS carries the same caveat, + and BOOTSTRAP's recon gains a git-identity probe. +- **Third-party notices.** `SKILLS/THIRD_PARTY_NOTICES.md` lists every work the + skills adapt, with its author, license, what OPS took and where the full + license text lives: the `grill-me`, `tdd` and `diagnosing-bugs` skills from + mattpocock/skills (MIT), the writing model `meta-skill-creator` names from + the same repository, the communication defaults adapted from i-have-adhd + (MIT), the AI-tell catalog adapted from cursor/plugins `unslop` (MIT), and + the design method adapted from anthropics/skills `frontend-design` + (Apache-2.0, whose license text OPS did not carry before and now reproduces). +- **A CI gate.** `.github/workflows/gate.yml` runs on every push and pull + request, with no secrets, so a fork's pull request gets the same run. Four + jobs: `syntax` (`bash -n`, `node --check`, `python3 -m py_compile` and a + PowerShell parse over every tracked script, found by extension or `#!` line; + an empty list fails the job), `suites` (every suite under + `bin/tests/`, the `*-selftest` scripts and the two PowerShell suites, the + destructive ones inside `sandbox-run`), `smoke` (a fresh install in a + throwaway `HOME` with the current Stage-1 settings, a git identity and + `deploy.sh --no-system` run twice, then `verify-ops.sh`), and `secrets` + (gitleaks over the full history, from a binary pinned by SHA-256 because the + official action wants a license key for organization repositories, then + OPS's own `secrets-scan.sh`). Actions are pinned by commit. Every job is + skipped outside the template repository, so private copies made from the + template do not spend runner minutes on every daily backup push. Not + covered: Windows and macOS hosts, and the Claude Code CLI. + ## 2026-10-18: hotfix release (current models, a gate that passes, deploy stops editing Stage 1) A fresh install against the current Stage-1 settings pinned retired models, From 4b271f7be1804bd37f4c291b5b7ae9ad1c63968e Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:48:30 -0400 Subject: [PATCH 06/19] ci(gate): check ES modules on any node; probe bwrap like sandbox-run Two defects found by running the gate's steps in an ubuntu:24.04 container instead of the maintainer's newer host: - The workflow scripts are ES modules (a top-level export). node --check treats a .js file as CommonJS unless the release detects module syntax, so Node 18 rejected four valid files and the result depended on the runner's node. A file that fails as a script is now checked again as a module; a real syntax error fails both ways. - The bubblewrap probe used only --ro-bind, which passes where sandbox-run's own setup (a PID and network namespace, /dev, a fresh /proc) does not, so a blocked sandbox would have shown up as every suite failing instead of at the probe. The probe now uses sandbox-run's flags, and its fallback and final check are unchanged. --- .github/workflows/gate.yml | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 80fc0ab..ed1dd6c 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -104,11 +104,20 @@ jobs: exit "$rc" - name: node --check on every .js + # The workflow scripts are ES modules (a top-level `export const meta`). + # node treats a .js file as CommonJS unless it detects module syntax, + # which only newer releases do by default (Node 18 rejects them), so a + # file that fails as a script is checked again as a module. A real syntax + # error fails both ways; the result no longer depends on the runner's node. run: | node --version + mkdir -p "$RUNNER_TEMP/esm" rc=0 while IFS= read -r f; do - node --check "$f" || { echo "::error file=$f::node --check failed"; rc=1; } + node --check "$f" 2>/dev/null && continue + cp "$f" "$RUNNER_TEMP/esm/check.mjs" + node --check "$RUNNER_TEMP/esm/check.mjs" \ + || { echo "::error file=$f::node --check failed (as a script and as a module)"; rc=1; } done < "$RUNNER_TEMP/node.lst" exit "$rc" @@ -158,10 +167,12 @@ jobs: # sandbox-run needs a root-owned bwrap. One root-wrapper case runs in zsh # (it prints SKIP without it), and the skill-mount tests exercise the # rsync engine as well as the tar fallback. Ubuntu 24.04 can restrict - # unprivileged user namespaces through AppArmor; if the plain probe - # fails, lift that restriction for this runner and probe again. The last - # probe is not optional: without a working sandbox the destructive-tool - # suites cannot run, and skipping them would hide a broken gate. + # unprivileged user namespaces through AppArmor; if the probe fails, + # lift that restriction for this runner and probe again. The probe uses + # the namespaces and mounts sandbox-run itself asks for (a new PID and + # network namespace, /dev and a fresh /proc). The last probe is not + # optional: without a working sandbox the destructive-tool suites cannot + # run, and skipping them would hide a broken gate. run: | need=() for pkg in bubblewrap:bwrap zsh:zsh rsync:rsync; do @@ -172,11 +183,12 @@ jobs: sudo apt-get install -y --no-install-recommends "${need[@]}" fi bwrap --version - if ! bwrap --ro-bind / / true 2>/dev/null; then + probe() { bwrap --die-with-parent --unshare-pid --unshare-net --new-session --ro-bind / / --dev /dev --proc /proc true; } + if ! probe 2>/dev/null; then echo "bwrap is blocked here; lifting the AppArmor user-namespace restriction" sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 fi - bwrap --ro-bind / / true + probe - name: Fetch the Stage-1 settings for the verify-ops self-test # verify-ops-selftest.sh also runs the shipped tree against a real From 96564e8509994925504e8eb309788d64d9546c36 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:48:52 -0400 Subject: [PATCH 07/19] docs(contributing): say when CI runs for a fork The gate's jobs are guarded to the template repository, so a push to a fork starts nothing and the run begins when the PR opens. Say so, next to the instruction to run the checks locally first. --- CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b95b007..36df1eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -179,6 +179,10 @@ deleter on `/` and `$HOME` and wiped most of a home directory. A throwaway PR and add it to `SKILLS/THIRD_PARTY_NOTICES.md` with its license, in the same change. - CI (`.github/workflows/gate.yml`) must be green before a merge. A failing job - is a defect in the change, or in the gate; do not merge around it. + is a defect in the change, or in the gate; do not merge around it. The jobs + run only on the template repository, so a push to your fork starts nothing: + the run begins when you open the PR (GitHub may hold a first-time + contributor's run until a maintainer approves it), which is why the checks + above come first. - This is a no-SLA repo (see README § Posture). PRs are welcome; response time isn't promised. From 521789a470cdaa72d0ed371b85d5f081723eec87 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:51:42 -0400 Subject: [PATCH 08/19] fix(workflows): close the context-coherence prompt in harness-audit.js The lane body for context-coherence was a template literal with no closing backtick, so the whole file was a syntax error ("Unexpected identifier 'LANE'" at the next lane). Every other lane ends its body with a backtick and a comma; this one now does too. It went unnoticed because node --check on a file that contains an export is a no-op on Node 24: the file is detected as a module and the check reports success even for an unclosed brace. The CI gate checks workflow scripts as the async function bodies they run as, which fails this file at origin/main and passes it with this change. --- .claude-config/workflows/harness-audit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude-config/workflows/harness-audit.js b/.claude-config/workflows/harness-audit.js index 1589ec7..979d4a1 100644 --- a/.claude-config/workflows/harness-audit.js +++ b/.claude-config/workflows/harness-audit.js @@ -85,7 +85,7 @@ Questions: (1) Per skill: does SKILL.md frontmatter/trigger description match wh key: 'context-coherence', body: `LANE: doctrine/documentation coherence across the instruction chain. Files: ~/OPS/AGENTS.md, ~/OPS/README.md, ~/OPS/DEPLOYMENT.md, ~/OPS/BOOTSTRAP.md, ~/OPS/CHANGELOG.md (if present), all of ~/OPS/CONTEXT/*.md (core.md, how-i-work.md, standing-orders.md, USER.md, voice.md, model-roles.md, project-kata.md, slots.md and the slot files), ~/linuxploitacious/claude/.claude/CLAUDE.md, ~/.claude/rules/ (list + readlink), plus ~/OPS/CONTEXT/projects/ (list only + spot-read 2 if any exist). -Questions: (1) Build a duplication map: which normative rules exist in 3+ places (compression rules, sycophancy bans, model-tier tables, commit standards, question-tool patterns, memory routing) and which copies have drifted from their declared single home (core.md for always-on rules, how-i-work.md for the why). Quote drifted pairs. (2) Contradictions/tensions: AGENTS.md repo instructions vs core.md "Intake, then autonomy" (plan and go, then autonomous execution); any file that still tells a session to wait where the core says run; TodoWrite vs TaskCreate naming (which is current?). (3) Stale factual claims: auto-compaction described as off (it is on as a backstop), retired models (Sonnet 5, Opus 4.8) or an Opus-default lane (the default lane is Sonnet 5.5), references to retired files (foreman-charter.md, boot-digest.md, operating-doctrine.md, working-preferences.md, about-me.md, brand-voice.md, WORKFORCE/), stale line-number references, stale counts ("X principles"). (4) README vs reality: folder tree accuracy, CHANGELOG discipline (does it reflect recent commits?). (5) Load-order sanity: what a session is told to read vs what is already loaded (core.md through ~/.claude/rules; the Stage-1 CLAUDE.md fallback read; AGENTS.md in ~/OPS): flag double loads and double reads. (6) The identity layer (USER.md, voice.md, standing-orders.md): internal quality, and whether a standing order contradicts core.md without saying it overrides a how-i-work.md default. (7) For each drift cluster, name ONE consolidation move (which copy becomes a pointer). +Questions: (1) Build a duplication map: which normative rules exist in 3+ places (compression rules, sycophancy bans, model-tier tables, commit standards, question-tool patterns, memory routing) and which copies have drifted from their declared single home (core.md for always-on rules, how-i-work.md for the why). Quote drifted pairs. (2) Contradictions/tensions: AGENTS.md repo instructions vs core.md "Intake, then autonomy" (plan and go, then autonomous execution); any file that still tells a session to wait where the core says run; TodoWrite vs TaskCreate naming (which is current?). (3) Stale factual claims: auto-compaction described as off (it is on as a backstop), retired models (Sonnet 5, Opus 4.8) or an Opus-default lane (the default lane is Sonnet 5.5), references to retired files (foreman-charter.md, boot-digest.md, operating-doctrine.md, working-preferences.md, about-me.md, brand-voice.md, WORKFORCE/), stale line-number references, stale counts ("X principles"). (4) README vs reality: folder tree accuracy, CHANGELOG discipline (does it reflect recent commits?). (5) Load-order sanity: what a session is told to read vs what is already loaded (core.md through ~/.claude/rules; the Stage-1 CLAUDE.md fallback read; AGENTS.md in ~/OPS): flag double loads and double reads. (6) The identity layer (USER.md, voice.md, standing-orders.md): internal quality, and whether a standing order contradicts core.md without saying it overrides a how-i-work.md default. (7) For each drift cluster, name ONE consolidation move (which copy becomes a pointer).`, }, { key: 'memory-audit', From 283e52604814d5a66cfdbd3f91729946a496a291 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:51:48 -0400 Subject: [PATCH 09/19] ci(gate): check workflow scripts as the async bodies they run as The previous node step (a second try as an ES module) was still wrong. The .js files here are workflow scripts with an export const meta and a body that uses top-level await and return. node --check on such a file passes vacuously on Node 24 (the export makes it a module and the check then reports no error, even for an unclosed brace) and fails the valid files on Node 18. Each file is now checked in the shape it runs in: export dropped, the body wrapped in an async function, that file given to node --check. A planted export plus an unclosed brace fails; the valid files pass on Node 18 and Node 24. --- .github/workflows/gate.yml | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index ed1dd6c..eb592b3 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -104,20 +104,28 @@ jobs: exit "$rc" - name: node --check on every .js - # The workflow scripts are ES modules (a top-level `export const meta`). - # node treats a .js file as CommonJS unless it detects module syntax, - # which only newer releases do by default (Node 18 rejects them), so a - # file that fails as a script is checked again as a module. A real syntax - # error fails both ways; the result no longer depends on the runner's node. + # The .js files here are workflow scripts: a top-level `export const + # meta`, and a body that uses top-level `await` and `return`, because the + # workflow runner executes the body as an async function. node --check on + # the file itself cannot be trusted for them. Node 18 rejects the valid + # files (it reads .js as CommonJS), and newer releases detect the + # `export`, treat the file as a module, and then report no error even for + # a file with an unclosed brace (checked on Node 24). So each file is + # checked in the shape it runs in: `export` dropped, the body wrapped in + # an async function, and that file given to node --check. Error line + # numbers are one more than the source's (the wrapper line). Any real + # syntax error fails on every node release. run: | node --version - mkdir -p "$RUNNER_TEMP/esm" rc=0 while IFS= read -r f; do - node --check "$f" 2>/dev/null && continue - cp "$f" "$RUNNER_TEMP/esm/check.mjs" - node --check "$RUNNER_TEMP/esm/check.mjs" \ - || { echo "::error file=$f::node --check failed (as a script and as a module)"; rc=1; } + { echo ';(async () => {' + sed -E -e '1{/^#!/d}' -e 's/^export[[:space:]]+//' "$f" + echo + echo '})' + } > "$RUNNER_TEMP/check.js" + node --check "$RUNNER_TEMP/check.js" \ + || { echo "::error file=$f::node --check failed (checked as an async function body)"; rc=1; } done < "$RUNNER_TEMP/node.lst" exit "$rc" From d3d3111b02c74d3be6429b9d31d0610419c6b9a2 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:51:51 -0400 Subject: [PATCH 10/19] docs: say how to check a workflow script; cite the push gate by step CONTRIBUTING and AGENTS told contributors to run node --check on workflow scripts, which is wrong for them (see the gate commit); they now give the wrapper command CI uses. README and AGENTS cited "The push gate", which is a bold paragraph inside step 8 of "What Stage 2 does", not a heading; both now say so. The CHANGELOG entry gains the fixed harness-audit.js prompt and the node check. --- AGENTS.md | 8 +++++--- CHANGELOG.md | 25 ++++++++++++++++--------- CONTRIBUTING.md | 13 ++++++++++++- README.md | 2 +- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3357c8f..e248732 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,8 +36,9 @@ current task allows. changed. A suite that runs a tool that deletes or moves files starts with the sandbox guard and runs only inside `.claude-config/bin/sandbox-run` (exit 97 elsewhere); the command line is in `.claude-config/bin/README.md`. - Every shell script you touch passes `bash -n`; every workflow script passes - `node --check`. In the template repository, CI + Every shell script you touch passes `bash -n`; every workflow script parses + (plain `node --check` is unreliable for them; CONTRIBUTING.md has the + command CI uses). In the template repository, CI (`.github/workflows/gate.yml`) runs the syntax checks, every suite, a fresh-install smoke test with `verify-ops.sh`, and a secrets scan on each push and pull request; CONTRIBUTING.md has the same commands for a local run. @@ -49,7 +50,8 @@ current task allows. - This copy pushes only to its own private origin. The git-guard hook runs `.claude-config/bin/origin-private-check` on the agent pushes it can parse here and blocks one it cannot verify, or one to another remote. It is a - seatbelt, not a sandbox: DEPLOYMENT.md "Push gate" lists its limits. + seatbelt, not a sandbox: DEPLOYMENT.md, "What Stage 2 does" step 8 ("The + push gate"), lists its limits. - Contributions to the public template go by PR and follow [CONTRIBUTING.md](CONTRIBUTING.md): port the mechanic, never paste the file, keep your private-term list out of every PR and commit, and push the PR branch diff --git a/CHANGELOG.md b/CHANGELOG.md index 642ee68..5bbf4b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -449,6 +449,11 @@ Your copy holds your memory and notes, so every push of it meets one gate. means anything: the gate reads host state from `$HOME`, so a machine with no deployed OPS needs the throwaway-`HOME` run. AGENTS carries the same caveat, and BOOTSTRAP's recon gains a git-identity probe. +- **A workflow script that did not parse.** The `context-coherence` lane prompt + in `.claude-config/workflows/harness-audit.js` had no closing backtick, so + the file was a syntax error; `node --check` on Node 24 had been reporting it + clean. The backtick is restored. The new `syntax` job catches this class, and + CONTRIBUTING and AGENTS now say how to check a workflow script. - **Third-party notices.** `SKILLS/THIRD_PARTY_NOTICES.md` lists every work the skills adapt, with its author, license, what OPS took and where the full license text lives: the `grill-me`, `tdd` and `diagnosing-bugs` skills from @@ -461,17 +466,19 @@ Your copy holds your memory and notes, so every push of it meets one gate. request, with no secrets, so a fork's pull request gets the same run. Four jobs: `syntax` (`bash -n`, `node --check`, `python3 -m py_compile` and a PowerShell parse over every tracked script, found by extension or `#!` line; - an empty list fails the job), `suites` (every suite under - `bin/tests/`, the `*-selftest` scripts and the two PowerShell suites, the - destructive ones inside `sandbox-run`), `smoke` (a fresh install in a - throwaway `HOME` with the current Stage-1 settings, a git identity and + an empty list fails the job; the workflow scripts are checked as the async + function bodies they run as, because plain `node --check` rejects them on Node + 18 and, on newer Node, reports no error for a broken one), `suites` (every + suite under `bin/tests/`, the `*-selftest` scripts and the two PowerShell + suites, the destructive ones inside `sandbox-run`), `smoke` (a fresh install + in a throwaway `HOME` with the current Stage-1 settings, a git identity and `deploy.sh --no-system` run twice, then `verify-ops.sh`), and `secrets` (gitleaks over the full history, from a binary pinned by SHA-256 because the - official action wants a license key for organization repositories, then - OPS's own `secrets-scan.sh`). Actions are pinned by commit. Every job is - skipped outside the template repository, so private copies made from the - template do not spend runner minutes on every daily backup push. Not - covered: Windows and macOS hosts, and the Claude Code CLI. + official action wants a license key for organization repositories, then OPS's + own `secrets-scan.sh`). Actions are pinned by commit. Every job is skipped + outside the template repository, so private copies made from the template do + not spend runner minutes on every daily backup push. Not covered: Windows and + macOS hosts, and the Claude Code CLI. ## 2026-10-18: hotfix release (current models, a gate that passes, deploy stops editing Stage 1) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36df1eb..42ed822 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,10 +73,21 @@ what you touched: ```bash bash -n -node --check python3 -m py_compile # use PYTHONPYCACHEPREFIX=/tmp/pyc to keep __pycache__ out of the tree ``` +Every tracked `.js` file is a workflow script: an `export const meta` and a body +that uses top-level `await` and `return`, because the workflow runner runs the +body as an async function. Plain `node --check ` is the wrong check for +them. Node 18 rejects the valid files, and newer releases detect the `export` +and then report no error even for a file with an unclosed brace. Check the body +in the shape it runs in (error line numbers come out one higher than the +source's): + +```bash +{ echo ';(async () => {'; sed -E -e '1{/^#!/d}' -e 's/^export[[:space:]]+//' ; echo; echo '})'; } > /tmp/check.js && node --check /tmp/check.js +``` + **2. Every suite.** Run from your checkout root. This is the CI command; it exits non-zero if any suite fails. It needs `bwrap` (`sudo apt install bubblewrap`; see "Where destructive tests run" below) and, for the last loop, diff --git a/README.md b/README.md index 473f9c3..1bde3df 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ you're working on. It has to be private. The automatic pushes (the daily backup and memory-init) check this first: they commit locally but push only when `gh` confirms origin is a private GitHub repo, and they refuse the public template itself, so a tryout clone never publishes your notes -(`DEPLOYMENT.md`, "The push gate"). +(`DEPLOYMENT.md`, "What Stage 2 does" step 8, "The push gate"). ### 2. Deploy it on your machine From af19a07d6d87b4aac6d00427193dd0b8d1f83ac0 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:55:05 -0400 Subject: [PATCH 11/19] ci(gate): upgrade an older git before the suites branch-gc reads git rev-parse --show-ref-format (git 2.45) and skips every repository without it. Ubuntu 24.04's own git is 2.43, and in an ubuntu:24.04 container 71 branch-gc assertions failed for that reason, with output that never named it. The suites job now checks the version, upgrades from the git-core PPA when it is older (the runner image normally ships a newer git, so this is usually a no-op), and fails with a clear message if it still is not 2.45. --- .github/workflows/gate.yml | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index eb592b3..49cbafd 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -171,25 +171,38 @@ jobs: with: persist-credentials: false - - name: Install bubblewrap, zsh and rsync, and prove bubblewrap can build a sandbox + - name: Install bubblewrap, zsh, rsync and a current git, and prove bubblewrap can build a sandbox # sandbox-run needs a root-owned bwrap. One root-wrapper case runs in zsh # (it prints SKIP without it), and the skill-mount tests exercise the - # rsync engine as well as the tar fallback. Ubuntu 24.04 can restrict - # unprivileged user namespaces through AppArmor; if the probe fails, - # lift that restriction for this runner and probe again. The probe uses - # the namespaces and mounts sandbox-run itself asks for (a new PID and - # network namespace, /dev and a fresh /proc). The last probe is not - # optional: without a working sandbox the destructive-tool suites cannot - # run, and skipping them would hide a broken gate. + # rsync engine as well as the tar fallback. branch-gc reads `git rev-parse + # --show-ref-format` (git 2.45) and skips every repository without it; + # Ubuntu 24.04's own git is 2.43, so an older git is upgraded from the + # git-core PPA (the runner image normally ships a newer one already). + # Ubuntu 24.04 can restrict unprivileged user namespaces through + # AppArmor; if the probe fails, lift that restriction for this runner and + # probe again. The probe uses the namespaces and mounts sandbox-run itself + # asks for (a new PID and network namespace, /dev and a fresh /proc). The + # last probe is not optional: without a working sandbox the + # destructive-tool suites cannot run, and skipping them would hide a + # broken gate. run: | need=() for pkg in bubblewrap:bwrap zsh:zsh rsync:rsync; do command -v "${pkg#*:}" >/dev/null 2>&1 || need+=("${pkg%%:*}") done - if [ "${#need[@]}" -gt 0 ]; then + git_ok() { [ "$(printf '%s\n2.45\n' "$(git --version | awk '{print $3}')" | sort -V | head -n 1)" = 2.45 ]; } + if [ "${#need[@]}" -gt 0 ] || ! git_ok; then sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends "${need[@]}" + [ "${#need[@]}" -eq 0 ] || sudo apt-get install -y --no-install-recommends "${need[@]}" fi + if ! git_ok; then + sudo apt-get install -y --no-install-recommends software-properties-common + sudo add-apt-repository -y ppa:git-core/ppa + sudo apt-get update -qq + sudo apt-get install -y git + fi + git --version + git_ok || { echo "::error::git 2.45 or newer is required (branch-gc reads --show-ref-format)"; exit 1; } bwrap --version probe() { bwrap --die-with-parent --unshare-pid --unshare-net --new-session --ro-bind / / --dev /dev --proc /proc true; } if ! probe 2>/dev/null; then From 3e0e6dab43912e5e3285eccb5e7b59cb85696c28 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 08:55:09 -0400 Subject: [PATCH 12/19] docs: state the git 2.45 floor for branch cleanup branch-gc needs git 2.45 or newer and, on an older git such as Ubuntu 24.04's 2.43, skips every repository and fails the nightly branch-cleanup stage (the log names the skip). README's requirements and DEPLOYMENT step 8 now say so, and the CHANGELOG entry records it. --- CHANGELOG.md | 60 +++++++++++++++++++++++++++------------------------ DEPLOYMENT.md | 9 ++++---- README.md | 5 ++++- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bbf4b9..ebb5c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -422,7 +422,6 @@ Your copy holds your memory and notes, so every push of it meets one gate. permissions across a pull. ### Docs, notices and CI - - **No em dashes.** The 139 em dashes still in tracked text (16 files: the `CONTEXT/` templates and the project kata, comments in hooks, scripts and self-tests, the design brand template, and two READMEs) are rewritten with @@ -431,24 +430,27 @@ Your copy holds your memory and notes, so every push of it meets one gate. self-tests' result lines, `sync-check.sh`'s messages, the `ops-verify` unit description), and no test greps any of those strings. - **The docs match the code.** README gains the requirements (git with an - identity, a signed-in `gh`, `python3`, `jq`), the T3 Code path, and sections - on what deploy does, the push gate and its limits, the daily backup, skill - mounts, template sync, and how to check a copy; its list of what the git - guard blocks now includes a bare `tmux kill-server` and an unverified push. - DEPLOYMENT said the trust anchor lives in `~/.claude/.claude.json`; deploy - writes `~/.claude.json`, beside the directory. It also called a failed skill - clone a warning, though that fails the deploy (`skills-mount`). It gains the - exact outputs of its verification commands, a recipe for testing a deploy - under a throwaway `HOME`, and a corrected count of the machinery deploy does - not wire. CONTRIBUTING gains the private-term rule (your list of private - terms never goes in a file, a commit message or a PR), a section on where - destructive tests run and why, and the `GIT_GUARD=off` rule for a PR branch. - Its suite loop printed `FAILED` but always exited 0; the command now counts - failures and exits non-zero, and runs the self-tests and the PowerShell - suites too. It also no longer implies that `verify-ops.sh` on a bare checkout - means anything: the gate reads host state from `$HOME`, so a machine with no - deployed OPS needs the throwaway-`HOME` run. AGENTS carries the same caveat, - and BOOTSTRAP's recon gains a git-identity probe. + identity and git 2.45 or newer, a signed-in `gh`, `python3`, `jq`), the T3 + Code path, and sections on what deploy does, the push gate and its limits, the + daily backup, skill mounts, template sync, and how to check a copy; its list + of what the git guard blocks now includes a bare `tmux kill-server` and an + unverified push. DEPLOYMENT said the trust anchor lives in + `~/.claude/.claude.json`; deploy writes `~/.claude.json`, beside the + directory. It also called a failed skill clone a warning, though that fails + the deploy (`skills-mount`). It gains the exact outputs of its verification + commands, a recipe for testing a deploy under a throwaway `HOME`, and a + corrected count of the machinery deploy does not wire. CONTRIBUTING gains the + private-term rule (your list of private terms never goes in a file, a commit + message or a PR), a section on where destructive tests run and why, and the + `GIT_GUARD=off` rule for a PR branch. Its suite loop printed `FAILED` but + always exited 0; the command now counts failures and exits non-zero, and runs + the self-tests and the PowerShell suites too. It also no longer implies that + `verify-ops.sh` on a bare checkout means anything: the gate reads host state + from `$HOME`, so a machine with no deployed OPS needs the throwaway-`HOME` + run. AGENTS carries the same caveat, and BOOTSTRAP's recon gains a + git-identity probe. README and DEPLOYMENT also say that `branch-gc` needs git + 2.45 or newer: on an older git, such as Ubuntu 24.04's 2.43, it skips every + repository (fail closed) and the nightly backup logs the skip. - **A workflow script that did not parse.** The `context-coherence` lane prompt in `.claude-config/workflows/harness-audit.js` had no closing backtick, so the file was a syntax error; `node --check` on Node 24 had been reporting it @@ -470,15 +472,17 @@ Your copy holds your memory and notes, so every push of it meets one gate. function bodies they run as, because plain `node --check` rejects them on Node 18 and, on newer Node, reports no error for a broken one), `suites` (every suite under `bin/tests/`, the `*-selftest` scripts and the two PowerShell - suites, the destructive ones inside `sandbox-run`), `smoke` (a fresh install - in a throwaway `HOME` with the current Stage-1 settings, a git identity and - `deploy.sh --no-system` run twice, then `verify-ops.sh`), and `secrets` - (gitleaks over the full history, from a binary pinned by SHA-256 because the - official action wants a license key for organization repositories, then OPS's - own `secrets-scan.sh`). Actions are pinned by commit. Every job is skipped - outside the template repository, so private copies made from the template do - not spend runner minutes on every daily backup push. Not covered: Windows and - macOS hosts, and the Claude Code CLI. + suites, the destructive ones inside `sandbox-run`; the job installs + bubblewrap, zsh and rsync and upgrades an older git, because `branch-gc` needs + 2.45), `smoke` (a fresh install in a throwaway `HOME` with the current Stage-1 + settings, a git identity and `deploy.sh --no-system` run twice, then + `verify-ops.sh`), and `secrets` (gitleaks over the full history, from a binary + pinned by SHA-256 because the official action wants a license key for + organization repositories, then OPS's own `secrets-scan.sh`). Actions are + pinned by commit. Every job is skipped outside the template repository, so + private copies made from the template do not spend runner minutes on every + daily backup push. Not covered: Windows and macOS hosts, and the Claude Code + CLI. ## 2026-10-18: hotfix release (current models, a gate that passes, deploy stops editing Stage 1) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 88a5dde..68c2bd9 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -195,10 +195,11 @@ In order: push gate verifies it private (no-op if clean; an origin with no branch of that name yet gets it as a first push), pulls every project repo, then runs `branch-gc --apply` to remove merged branches and park their worktrees in a - 7-day trash (it needs a signed-in `gh` and removes nothing without one). It - logs to `.claude-config/backup/sync.log`, and always exits 0, so the - session briefing's `Backup:` line repeats the last run's OPS-stage errors - and skipped pushes at boot. Skipped with `--no-system`. + 7-day trash (it needs a signed-in `gh` and git 2.45 or newer, and removes + nothing without them: an older git skips every repository, and the stage + logs the skip). It logs to `.claude-config/backup/sync.log`, and always + exits 0, so the session briefing's `Backup:` line repeats the last run's + OPS-stage errors and skipped pushes at boot. Skipped with `--no-system`. **The push gate.** Your copy holds your memory, context, and notes, so the two automatic pushes (memory-init's and the daily backup's) run only when diff --git a/README.md b/README.md index 1bde3df..ef725f2 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,10 @@ Git Bash), and these on the machine: often has none, and then every commit fails: the memory sync's, the daily backup's, and every agent's. Deploy asks `git var` for the author and committer identity and, when either is missing, prints those two commands and - finishes with a failure named `git-identity` (`DEPLOYMENT.md`, step 7). + finishes with a failure named `git-identity` (`DEPLOYMENT.md`, step 7). Use + git 2.45 or newer: branch cleanup reads `git rev-parse --show-ref-format` and, + on an older git (Ubuntu 24.04 ships 2.43), skips every repository and logs the + skip; the rest of OPS works either way. - **`gh`, signed in (`gh auth login`).** The private-origin check and branch cleanup ask GitHub through it. Without a signed-in `gh`, the automatic pushes stay local and branch cleanup removes nothing. From 3e7f06ad9530ef38a6e44c02325ae4c9e8ef5930 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 09:05:01 -0400 Subject: [PATCH 13/19] ci(gate): run on ubuntu-26.04, where branch-gc can run branch-gc quarantines a worktree with mv --no-copy --update=none-fail. --update=none-fail is new in GNU coreutils 9.5, and ubuntu-24.04 ships 9.4, so there mv rejects the option ("invalid argument 'none-fail' for '--update'") and 34 branch-gc assertions across four suites fail; in an ubuntu:24.04 container the other 21 suites pass. ubuntu-latest still means 24.04 and is announced to move to 26.04 in November 2026. All four jobs name ubuntu-26.04, the image the maintainer's own host matches, so the local run of these steps is representative. --- .github/workflows/gate.yml | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 49cbafd..4c8ac53 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -55,9 +55,14 @@ jobs: syntax: name: syntax if: github.repository == 'Exploitacious/OPS' - # A named image, not ubuntu-latest: the sandbox tests depend on how Ubuntu's - # AppArmor treats bubblewrap, and a silent image change should not move them. - runs-on: ubuntu-24.04 + # Ubuntu 26.04 by name, not ubuntu-latest (which still means 24.04 and is + # announced to move to 26.04 in November 2026). branch-gc quarantines a + # worktree with `mv --no-copy --update=none-fail`, which GNU coreutils has + # only since 9.5; 24.04 ships 9.4, where `mv` rejects the option and every + # branch-gc suite fails. The sandbox tests also depend on how the + # distribution's AppArmor treats bubblewrap, so a silent image change should + # not move them either. + runs-on: ubuntu-26.04 timeout-minutes: 10 steps: - name: Check out the repository @@ -163,7 +168,7 @@ jobs: suites: name: suites if: github.repository == 'Exploitacious/OPS' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 45 steps: - name: Check out the repository @@ -177,14 +182,14 @@ jobs: # rsync engine as well as the tar fallback. branch-gc reads `git rev-parse # --show-ref-format` (git 2.45) and skips every repository without it; # Ubuntu 24.04's own git is 2.43, so an older git is upgraded from the - # git-core PPA (the runner image normally ships a newer one already). - # Ubuntu 24.04 can restrict unprivileged user namespaces through - # AppArmor; if the probe fails, lift that restriction for this runner and - # probe again. The probe uses the namespaces and mounts sandbox-run itself - # asks for (a new PID and network namespace, /dev and a fresh /proc). The - # last probe is not optional: without a working sandbox the - # destructive-tool suites cannot run, and skipping them would hide a - # broken gate. + # git-core PPA (the 26.04 runner image already ships 2.55, so this is + # normally a no-op). Ubuntu can restrict unprivileged user namespaces + # through AppArmor: if the probe fails, lift that restriction for this + # runner and probe again. The probe uses the namespaces and mounts + # sandbox-run itself asks for (a new PID and network namespace, /dev and + # a fresh /proc). The last probe is not optional: without a working + # sandbox the destructive-tool suites cannot run, and skipping them would + # hide a broken gate. run: | need=() for pkg in bubblewrap:bwrap zsh:zsh rsync:rsync; do @@ -266,7 +271,7 @@ jobs: smoke: name: fresh install, then verify-ops if: github.repository == 'Exploitacious/OPS' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 15 steps: - name: Check out the repository @@ -304,7 +309,7 @@ jobs: secrets: name: secrets if: github.repository == 'Exploitacious/OPS' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 20 steps: - name: Check out the full history From c465ce2d5f5f5e13fed80b1d2393dc3933109d50 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 09:05:05 -0400 Subject: [PATCH 14/19] docs: add GNU coreutils 9.5 to the branch cleanup requirements branch-gc needs git 2.45 for --show-ref-format and GNU coreutils 9.5 for mv --update=none-fail. On Ubuntu 24.04 (git 2.43, coreutils 9.4) it skips every repository or cannot move a worktree to the trash, and logs it. README's requirements, DEPLOYMENT step 8 and the CHANGELOG entry say so, and the CHANGELOG entry records the runner image the gate uses. --- CHANGELOG.md | 82 ++++++++++++++++++++++++++------------------------- DEPLOYMENT.md | 11 +++---- README.md | 10 ++++--- 3 files changed, 54 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb5c96..7cca193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -430,27 +430,29 @@ Your copy holds your memory and notes, so every push of it meets one gate. self-tests' result lines, `sync-check.sh`'s messages, the `ops-verify` unit description), and no test greps any of those strings. - **The docs match the code.** README gains the requirements (git with an - identity and git 2.45 or newer, a signed-in `gh`, `python3`, `jq`), the T3 - Code path, and sections on what deploy does, the push gate and its limits, the - daily backup, skill mounts, template sync, and how to check a copy; its list - of what the git guard blocks now includes a bare `tmux kill-server` and an - unverified push. DEPLOYMENT said the trust anchor lives in - `~/.claude/.claude.json`; deploy writes `~/.claude.json`, beside the - directory. It also called a failed skill clone a warning, though that fails - the deploy (`skills-mount`). It gains the exact outputs of its verification - commands, a recipe for testing a deploy under a throwaway `HOME`, and a - corrected count of the machinery deploy does not wire. CONTRIBUTING gains the - private-term rule (your list of private terms never goes in a file, a commit - message or a PR), a section on where destructive tests run and why, and the - `GIT_GUARD=off` rule for a PR branch. Its suite loop printed `FAILED` but - always exited 0; the command now counts failures and exits non-zero, and runs - the self-tests and the PowerShell suites too. It also no longer implies that - `verify-ops.sh` on a bare checkout means anything: the gate reads host state - from `$HOME`, so a machine with no deployed OPS needs the throwaway-`HOME` - run. AGENTS carries the same caveat, and BOOTSTRAP's recon gains a - git-identity probe. README and DEPLOYMENT also say that `branch-gc` needs git - 2.45 or newer: on an older git, such as Ubuntu 24.04's 2.43, it skips every - repository (fail closed) and the nightly backup logs the skip. + identity, a signed-in `gh`, `python3`, `jq`, and git 2.45 and GNU coreutils + 9.5 for branch cleanup), the T3 Code path, and sections on what deploy does, + the push gate and its limits, the daily backup, skill mounts, template sync, + and how to check a copy; its list of what the git guard blocks now includes a + bare `tmux kill-server` and an unverified push. DEPLOYMENT said the trust + anchor lives in `~/.claude/.claude.json`; deploy writes `~/.claude.json`, + beside the directory. It also called a failed skill clone a warning, though + that fails the deploy (`skills-mount`). It gains the exact outputs of its + verification commands, a recipe for testing a deploy under a throwaway `HOME`, + and a corrected count of the machinery deploy does not wire. CONTRIBUTING + gains the private-term rule (your list of private terms never goes in a file, + a commit message or a PR), a section on where destructive tests run and why, + and the `GIT_GUARD=off` rule for a PR branch. Its suite loop printed `FAILED` + but always exited 0; the command now counts failures and exits non-zero, and + runs the self-tests and the PowerShell suites too. It also no longer implies + that `verify-ops.sh` on a bare checkout means anything: the gate reads host + state from `$HOME`, so a machine with no deployed OPS needs the + throwaway-`HOME` run. AGENTS carries the same caveat, and BOOTSTRAP's recon + gains a git-identity probe. README and DEPLOYMENT also say that `branch-gc` + needs git 2.45 or newer and GNU coreutils 9.5 or newer: on Ubuntu 24.04 (git + 2.43, coreutils 9.4) it skips every repository, or cannot move a worktree to + the trash because `mv --update=none-fail` is new in 9.5, and the nightly + backup logs it. - **A workflow script that did not parse.** The `context-coherence` lane prompt in `.claude-config/workflows/harness-audit.js` had no closing backtick, so the file was a syntax error; `node --check` on Node 24 had been reporting it @@ -465,25 +467,25 @@ Your copy holds your memory and notes, so every push of it meets one gate. the design method adapted from anthropics/skills `frontend-design` (Apache-2.0, whose license text OPS did not carry before and now reproduces). - **A CI gate.** `.github/workflows/gate.yml` runs on every push and pull - request, with no secrets, so a fork's pull request gets the same run. Four - jobs: `syntax` (`bash -n`, `node --check`, `python3 -m py_compile` and a - PowerShell parse over every tracked script, found by extension or `#!` line; - an empty list fails the job; the workflow scripts are checked as the async - function bodies they run as, because plain `node --check` rejects them on Node - 18 and, on newer Node, reports no error for a broken one), `suites` (every - suite under `bin/tests/`, the `*-selftest` scripts and the two PowerShell - suites, the destructive ones inside `sandbox-run`; the job installs - bubblewrap, zsh and rsync and upgrades an older git, because `branch-gc` needs - 2.45), `smoke` (a fresh install in a throwaway `HOME` with the current Stage-1 - settings, a git identity and `deploy.sh --no-system` run twice, then - `verify-ops.sh`), and `secrets` (gitleaks over the full history, from a binary - pinned by SHA-256 because the official action wants a license key for - organization repositories, then OPS's own `secrets-scan.sh`). Actions are - pinned by commit. Every job is skipped outside the template repository, so - private copies made from the template do not spend runner minutes on every - daily backup push. Not covered: Windows and macOS hosts, and the Claude Code - CLI. - + request, with no secrets, so a fork's pull request gets the same run, on the + `ubuntu-26.04` image (24.04's coreutils 9.4 cannot run `branch-gc`, and + `ubuntu-latest` still means 24.04). Four jobs: `syntax` (`bash -n`, `node + --check`, `python3 -m py_compile` and a PowerShell parse over every tracked + script, found by extension or `#!` line; an empty list fails the job; the + workflow scripts are checked as the async function bodies they run as, because + plain `node --check` rejects them on Node 18 and, on newer Node, reports no + error for a broken one), `suites` (every suite under `bin/tests/`, the + `*-selftest` scripts and the two PowerShell suites, the destructive ones + inside `sandbox-run`; the job installs bubblewrap, zsh and rsync and upgrades + an older git, because `branch-gc` needs 2.45), `smoke` (a fresh install in a + throwaway `HOME` with the current Stage-1 settings, a git identity and + `deploy.sh --no-system` run twice, then `verify-ops.sh`), and `secrets` + (gitleaks over the full history, from a binary pinned by SHA-256 because the + official action wants a license key for organization repositories, then OPS's + own `secrets-scan.sh`). Actions are pinned by commit. Every job is skipped + outside the template repository, so private copies made from the template do + not spend runner minutes on every daily backup push. Not covered: Windows and + macOS hosts, and the Claude Code CLI. ## 2026-10-18: hotfix release (current models, a gate that passes, deploy stops editing Stage 1) A fresh install against the current Stage-1 settings pinned retired models, diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 68c2bd9..4187d6d 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -195,11 +195,12 @@ In order: push gate verifies it private (no-op if clean; an origin with no branch of that name yet gets it as a first push), pulls every project repo, then runs `branch-gc --apply` to remove merged branches and park their worktrees in a - 7-day trash (it needs a signed-in `gh` and git 2.45 or newer, and removes - nothing without them: an older git skips every repository, and the stage - logs the skip). It logs to `.claude-config/backup/sync.log`, and always - exits 0, so the session briefing's `Backup:` line repeats the last run's - OPS-stage errors and skipped pushes at boot. Skipped with `--no-system`. + 7-day trash. It needs a signed-in `gh`, git 2.45 or newer and GNU coreutils + 9.5 or newer: without `gh` it removes nothing, an older git skips every + repository, and an older `mv` cannot move a worktree to the trash; each case + is logged. The sync script logs to `.claude-config/backup/sync.log`, and + always exits 0, so the session briefing's `Backup:` line repeats the last + run's OPS-stage errors and skipped pushes at boot. Skipped with `--no-system`. **The push gate.** Your copy holds your memory, context, and notes, so the two automatic pushes (memory-init's and the daily backup's) run only when diff --git a/README.md b/README.md index ef725f2..5fd42ac 100644 --- a/README.md +++ b/README.md @@ -105,10 +105,12 @@ Git Bash), and these on the machine: often has none, and then every commit fails: the memory sync's, the daily backup's, and every agent's. Deploy asks `git var` for the author and committer identity and, when either is missing, prints those two commands and - finishes with a failure named `git-identity` (`DEPLOYMENT.md`, step 7). Use - git 2.45 or newer: branch cleanup reads `git rev-parse --show-ref-format` and, - on an older git (Ubuntu 24.04 ships 2.43), skips every repository and logs the - skip; the rest of OPS works either way. + finishes with a failure named `git-identity` (`DEPLOYMENT.md`, step 7). Branch + cleanup also wants git 2.45 or newer (it reads `git rev-parse + --show-ref-format`) and GNU coreutils 9.5 or newer (it quarantines a worktree + with `mv --update=none-fail`). On Ubuntu 24.04 (git 2.43, coreutils 9.4) it + skips every repository, or fails to move a worktree, and logs it; the rest of + OPS works either way. - **`gh`, signed in (`gh auth login`).** The private-origin check and branch cleanup ask GitHub through it. Without a signed-in `gh`, the automatic pushes stay local and branch cleanup removes nothing. From ff12047a74c520cc8f28130532d1a9c801932b16 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 10:28:09 -0400 Subject: [PATCH 15/19] docs(contributing): exit non-zero when a suite fails The "Every suite" command ended with a bare echo of the failure count, so its exit status was always 0 and a red run looked green to a script, a wrapper or an && chain. CONTRIBUTING and the changelog both said it exits non-zero. End the block with [ "$fails" -eq 0 ], as gate.yml's suite steps do, so the claim is true. Checked by running the documented block: with a failing suite its exit status is 1 (it was 0 at c465ce2), with only passing suites it stays 0. --- CHANGELOG.md | 11 ++++++----- CONTRIBUTING.md | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cca193..580d193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -443,11 +443,12 @@ Your copy holds your memory and notes, so every push of it meets one gate. gains the private-term rule (your list of private terms never goes in a file, a commit message or a PR), a section on where destructive tests run and why, and the `GIT_GUARD=off` rule for a PR branch. Its suite loop printed `FAILED` - but always exited 0; the command now counts failures and exits non-zero, and - runs the self-tests and the PowerShell suites too. It also no longer implies - that `verify-ops.sh` on a bare checkout means anything: the gate reads host - state from `$HOME`, so a machine with no deployed OPS needs the - throwaway-`HOME` run. AGENTS carries the same caveat, and BOOTSTRAP's recon + but always exited 0, because its last command was an `echo`; the command now + counts failures and ends with a test of the count, so a red run exits + non-zero, and it runs the self-tests and the PowerShell suites too. It also + no longer implies that `verify-ops.sh` on a bare checkout means anything: the + gate reads host state from `$HOME`, so a machine with no deployed OPS needs + the throwaway-`HOME` run. AGENTS carries the same caveat, and BOOTSTRAP's recon gains a git-identity probe. README and DEPLOYMENT also say that `branch-gc` needs git 2.45 or newer and GNU coreutils 9.5 or newer: on Ubuntu 24.04 (git 2.43, coreutils 9.4) it skips every repository, or cannot move a worktree to diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42ed822..1619c57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,6 +114,7 @@ for t in deploy-windows-test.ps1 memory-init-windows-test.ps1; do pwsh -NoProfile -File ".claude-config/bin/tests/$t" || { echo "FAILED: $t"; fails=$((fails + 1)); } done echo "failed suites: $fails" +[ "$fails" -eq 0 ] # last, so the exit status is the result (the echo exits 0) ``` `verify-ops-selftest.sh` also checks the shipped tree against the real Stage-1 From 270930f7d19077e1b7b84c907f0d4802bf6f0cdd Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 10:28:15 -0400 Subject: [PATCH 16/19] docs(deployment): keep the throwaway-HOME recipe from pushing The recipe is meant to run without touching your real setup, but a clone of a URL that is a private GitHub repo let deploy's memory sync pass the push gate and push a feat(memory): seed commit to the real origin. A new HOME hides neither a GH_TOKEN/GITHUB_TOKEN/GH_CONFIG_DIR login nor SSH keys, and --no-system only skips the crontab and root's rc files. Point the clone's push URL at /dev/null so the gate answers "origin is a local path", say why next to the recipe, and note it in the changelog. Checked by running the recipe against an emulated private origin: the origin head moved at c465ce2 and stays put with the new line. --- CHANGELOG.md | 7 +++++-- DEPLOYMENT.md | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 580d193..985b8d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -438,8 +438,11 @@ Your copy holds your memory and notes, so every push of it meets one gate. anchor lives in `~/.claude/.claude.json`; deploy writes `~/.claude.json`, beside the directory. It also called a failed skill clone a warning, though that fails the deploy (`skills-mount`). It gains the exact outputs of its - verification commands, a recipe for testing a deploy under a throwaway `HOME`, - and a corrected count of the machinery deploy does not wire. CONTRIBUTING + verification commands, a recipe for testing a deploy under a throwaway `HOME` + (its clone's push URL is set to `/dev/null`: a new `HOME` hides neither your + `gh` login nor your SSH keys, so a clone of a private origin would otherwise + get deploy's `feat(memory): seed` commit pushed to the real origin), and a + corrected count of the machinery deploy does not wire. CONTRIBUTING gains the private-term rule (your list of private terms never goes in a file, a commit message or a PR), a section on where destructive tests run and why, and the `GIT_GUARD=off` rule for a PR branch. Its suite loop printed `FAILED` diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 4187d6d..0b0ed8a 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -332,6 +332,7 @@ run it under a throwaway `HOME`. CI does exactly this on every pull request mkdir -p ~/.claude curl -fsSL https://raw.githubusercontent.com/Exploitacious/linuxploitacious/HEAD/claude/.claude/settings.json -o ~/.claude/settings.json git clone ~/OPS + git -C ~/OPS remote set-url --push origin /dev/null # see below: never push from the copy bash ~/OPS/.claude-config/deploy.sh --no-system bash ~/OPS/.claude-config/bin/verify-ops.sh ) @@ -345,6 +346,16 @@ rc files. Deploy still clones the public skill source over HTTPS, so the test needs network access, and the throwaway directory stays in your temp dir until you delete it. +Do not drop the `set-url --push` line when you clone a URL. Deploy's memory sync +commits to the copy and pushes it whenever the push gate finds origin private, +and a new `HOME` hides neither your GitHub login (an exported `GH_TOKEN` or +`GITHUB_TOKEN`, or `GH_CONFIG_DIR`) nor your SSH keys (ssh finds `~/.ssh` +through the passwd entry, not `$HOME`). Without the line, a clone of your +private repository gets a `feat(memory): seed .claude-memory/...` commit pushed +to the real origin, and `--no-system` does not prevent that. With the push URL +at `/dev/null` the gate answers "origin is a local path" and the commit stays in +the throwaway copy (the same answer a clone of a local path gets). + ## T3 Code hosts T3 Code runs Claude Code as a provider thread, so nothing on the OPS boot path From b341a8ce8340e6be598b02600e547eb9fbf77a0c Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 11:13:05 -0400 Subject: [PATCH 17/19] fix(ci): check JavaScript with a parse a body cannot escape The syntax job wrapped each workflow script in an async arrow function and ran node --check on the result. A body that closes the wrapper and opens another function passes that check while the script itself is invalid. The step now builds an AsyncFunction from the body, which parses without running it, and removes only the leading `export const meta`, so a malformed later export stays invalid. Any other .js is checked in explicit CommonJS, then module, mode, because Node 24's automatic module detection can accept an unclosed brace. A workflow script is chosen by its first statement, not its path. A path rule sent SKILLS/memory-prune/audit_workflow.js to the module check, which rejects its top-level return, so the gate failed on the repository's own tree. tests/ci-gate-test.py runs the step itself over every tracked .js and fifteen fixtures. Under sandbox-run it skips when node lives in HOME (nvm), and says so; in GitHub Actions a missing node fails it. CONTRIBUTING points at that test instead of a second copy of the parser, and the gate's header no longer says the bin README lists the same commands. --- .claude-config/bin/tests/ci-gate-test.py | 275 +++++++++++++++++++++++ .github/workflows/gate.yml | 62 +++-- CHANGELOG.md | 15 +- CONTRIBUTING.md | 20 +- 4 files changed, 336 insertions(+), 36 deletions(-) create mode 100755 .claude-config/bin/tests/ci-gate-test.py diff --git a/.claude-config/bin/tests/ci-gate-test.py b/.claude-config/bin/tests/ci-gate-test.py new file mode 100755 index 0000000..7cd8466 --- /dev/null +++ b/.claude-config/bin/tests/ci-gate-test.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +"""Exercise the CI JavaScript check against source, without running that source.""" + +import os +import shutil +import subprocess +import tempfile +import textwrap +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +GATE = ROOT / ".github/workflows/gate.yml" + + +def javascript_step(): + """Read this literal run block without adding a YAML package dependency.""" + lines = GATE.read_text().splitlines() + start = lines.index(" - name: Check every JavaScript script") + start = lines.index(" run: |", start) + 1 + body = [] + for line in lines[start:]: + if line and not line.startswith(" "): + break + body.append(line[10:]) + return "\n".join(body) + "\n" + + +class JavaScriptGateTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + # sandbox-run gives the suite a fresh HOME, so a node installed under + # the real one (nvm, fnm) is not on PATH there. Locally that is a skip + # with the way out; in GitHub Actions it is a failure, so the gate can + # never pass this suite without running it. + if shutil.which("node"): + return + if os.environ.get("GITHUB_ACTIONS") == "true": + raise RuntimeError("node is not on PATH in CI: the JavaScript check cannot be tested") + raise unittest.SkipTest( + "node is not on PATH here (under sandbox-run, a node installed in " + "your HOME is hidden); run python3 -B " + ".claude-config/bin/tests/ci-gate-test.py outside the sandbox" + ) + + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.root = Path(tmp.name) + self.runner = self.root / "runner" + self.runner.mkdir() + + def check(self, files): + for name, source in files.items(): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(textwrap.dedent(source)) + (self.runner / "node.lst").write_text("".join(name + "\n" for name in files)) + return subprocess.run( + [ + "bash", + "--noprofile", + "--norc", + "-eo", + "pipefail", + "-c", + javascript_step(), + ], + cwd=self.root, + env={**os.environ, "RUNNER_TEMP": str(self.runner)}, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + + def assert_result(self, result, valid): + self.assertEqual(result.returncode == 0, valid, result.stdout + result.stderr) + + def test_every_tracked_javascript_file_passes(self): + # The step over the repository's own .js files, so a checker change that + # rejects a shipped script fails here, not first on the hosted runner. + # This covers the skill-bundled workflow SKILLS/memory-prune/ + # audit_workflow.js, which a path rule once sent to the module check. + listed = subprocess.run( + ["git", "-C", str(ROOT), "ls-files", "-z", "*.js"], + capture_output=True, + text=True, + check=False, + ) + if listed.returncode == 0: + names = [name for name in listed.stdout.split("\0") if name] + else: + names = [ + str(path.relative_to(ROOT)) + for path in sorted(ROOT.rglob("*.js")) + if ".git" not in path.relative_to(ROOT).parts + ] + files = {name: (ROOT / name).read_text() for name in names} + self.assertIn("SKILLS/memory-prune/audit_workflow.js", files) + self.assert_result(self.check(files), True) + + def test_workflow_outside_the_workflows_directory(self): + self.assert_result( + self.check( + { + "SKILLS/demo/audit_workflow.js": """ + // A skill-bundled workflow script. + export const meta = { name: 'demo' }; + const value = await Promise.resolve(1); + return value; + """ + } + ), + True, + ) + + def test_module_exporting_meta_later_is_an_ordinary_module(self): + # Only a leading `export const meta =` makes a workflow script. + self.assert_result( + self.check( + { + "ordinary.js": """ + import fs from 'node:fs'; + export const meta = { fs }; + """ + } + ), + True, + ) + + def test_valid_workflow_await_and_return(self): + self.assert_result( + self.check( + { + ".claude-config/workflows/valid.js": """ + export const meta = {}; + const value = await Promise.resolve(1); + return value; + """ + } + ), + True, + ) + + def test_workflow_shebang_comments_and_indented_metadata(self): + source = ( + "#!/usr/bin/env node\n// Heading\n/* Multiple\nlines */\n" + " export const meta = {};\nreturn await Promise.resolve(1);\n" + ) + self.assert_result( + self.check({".claude-config/workflows/valid.js": source}), True + ) + + def test_nested_metadata_export_remains_invalid(self): + source = "function nested() {\nexport const meta = {};\n}\n" + self.assert_result( + self.check({".claude-config/workflows/broken.js": source}), False + ) + + def test_workflow_cannot_escape_a_textual_wrapper(self): + # A wrapper around this invalid body becomes two valid functions. + self.assert_result( + self.check( + { + ".claude-config/workflows/broken.js": """ + export const meta = {}; + }) + ;(async () => { + return 1; + """ + } + ), + False, + ) + + def test_malformed_export_is_not_stripped(self): + self.assert_result( + self.check( + { + ".claude-config/workflows/broken.js": """ + export const meta = {}; + export if (true) {} + """ + } + ), + False, + ) + + def test_valid_commonjs_await_identifier(self): + self.assert_result( + self.check({"ordinary.js": "var await = 1;\nconsole.log(await);\n"}), True + ) + + def test_valid_es_module(self): + self.assert_result( + self.check( + { + "ordinary.js": """ + import fs from 'node:fs'; + export default await Promise.resolve(fs); + """ + } + ), + True, + ) + + def test_broken_es_module_fails_before_a_valid_file(self): + self.assert_result( + self.check( + { + "broken.js": "export const value = {\n", + "valid.js": "const value = 1;\n", + } + ), + False, + ) + + def test_broken_module_with_import(self): + self.assert_result( + self.check( + { + "ordinary.js": """ + import fs from 'node:fs'; + export const value = { + """ + } + ), + False, + ) + + def test_static_import_cannot_hide_a_broken_workflow(self): + self.assert_result( + self.check( + { + ".claude-config/workflows/broken.js": """ + export const meta = {}; + import fs from 'node:fs'; + const value = { + """ + } + ), + False, + ) + + def test_unclosed_workflow_body(self): + self.assert_result( + self.check( + { + ".claude-config/workflows/broken.js": """ + export const meta = {}; + if (true) { + """ + } + ), + False, + ) + + def test_source_is_not_executed(self): + source = "require('node:fs').writeFileSync('executed', 'bad');\n" + self.assert_result( + self.check( + { + "ordinary.js": source, + ".claude-config/workflows/valid.js": "export const meta = {};\n" + + source, + } + ), + True, + ) + self.assertFalse((self.root / "executed").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 4c8ac53..9c6376f 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -1,6 +1,6 @@ # The OPS gate. Runs on every push and pull request: # -# syntax bash -n, node --check, python3 -m py_compile and a PowerShell parse +# syntax bash -n, a JavaScript parse, python3 -m py_compile and a PowerShell parse # over every tracked script # suites every suite under .claude-config/bin/tests/, the *-selftest scripts # and the PowerShell suites (the ones that delete or move files run @@ -23,8 +23,8 @@ # Not covered, on purpose: Windows and macOS (this runs on Linux; the two # PowerShell suites run under pwsh on Linux and exercise deploy.ps1's logic, not # a Windows host), and any Claude Code CLI behavior (the CLI is not installed; -# deploy warns and goes on without it). `.claude-config/bin/README.md` and -# CONTRIBUTING.md list the same commands for a local run. +# deploy warns and goes on without it). CONTRIBUTING.md ("Before you open the +# PR") lists the same commands for a local run. name: gate on: @@ -108,29 +108,47 @@ jobs: done < "$RUNNER_TEMP/shell.lst" exit "$rc" - - name: node --check on every .js - # The .js files here are workflow scripts: a top-level `export const - # meta`, and a body that uses top-level `await` and `return`, because the - # workflow runner executes the body as an async function. node --check on - # the file itself cannot be trusted for them. Node 18 rejects the valid - # files (it reads .js as CommonJS), and newer releases detect the - # `export`, treat the file as a module, and then report no error even for - # a file with an unclosed brace (checked on Node 24). So each file is - # checked in the shape it runs in: `export` dropped, the body wrapped in - # an async function, and that file given to node --check. Error line - # numbers are one more than the source's (the wrapper line). Any real - # syntax error fails on every node release. + - name: Check every JavaScript script + # A workflow script is any .js whose first statement (after a #! line, + # comments and blank lines) is `export const meta =`: the ones under + # .claude-config/workflows/ and skill-bundled ones such as + # SKILLS/memory-prune/audit_workflow.js. It is chosen by content, not + # path, because a path rule sends a skill's workflow to the module + # check, which rejects its top-level `return`. The workflow runner + # executes the body as an async function, with top-level `await` and + # `return`, so that body is parsed directly: constructing an + # AsyncFunction parses without running it, and unlike a textual wrapper + # it cannot accept a body that closes the wrapper and opens another + # function. Only the leading metadata export is removed, so a malformed + # later export stays invalid. The parser exits 10 for any other file, + # which is then checked in explicit CommonJS, then module, mode: Node + # 24's automatic module detection can accept an unclosed brace. run: | node --version rc=0 while IFS= read -r f; do - { echo ';(async () => {' - sed -E -e '1{/^#!/d}' -e 's/^export[[:space:]]+//' "$f" - echo - echo '})' - } > "$RUNNER_TEMP/check.js" - node --check "$RUNNER_TEMP/check.js" \ - || { echo "::error file=$f::node --check failed (checked as an async function body)"; rc=1; } + st=0 + node --input-type=commonjs - "$f" <<'JS' || st=$? + const { readFileSync } = require('node:fs'); + const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; + const source = readFileSync(process.argv[2], 'utf8') + .replace(/^#![^\r\n]*(?:\r?\n|$)/, '\n'); + const lead = source.match(/^(?:\s+|\/\/[^\r\n]*|\/\*[\s\S]*?\*\/)*/)[0]; + const rest = source.slice(lead.length); + if (!/^export\s+const\s+meta\s*=/.test(rest)) process.exit(10); + new AsyncFunction(lead + rest.replace(/^export(?=\s+const\s+meta\s*=)/, ' ')); + JS + case "$st" in + 0) ;; + 10) + if ! node --check --input-type=commonjs < "$f" 2> "$RUNNER_TEMP/node.err"; then + node --check --input-type=module < "$f" \ + || { cat "$RUNNER_TEMP/node.err"; echo "::error file=$f::invalid JavaScript"; rc=1; } + fi ;; + *) + echo "::error file=$f::invalid workflow async function body" + rc=1 ;; + esac done < "$RUNNER_TEMP/node.lst" exit "$rc" diff --git a/CHANGELOG.md b/CHANGELOG.md index 985b8d2..ea18c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -473,12 +473,15 @@ Your copy holds your memory and notes, so every push of it meets one gate. - **A CI gate.** `.github/workflows/gate.yml` runs on every push and pull request, with no secrets, so a fork's pull request gets the same run, on the `ubuntu-26.04` image (24.04's coreutils 9.4 cannot run `branch-gc`, and - `ubuntu-latest` still means 24.04). Four jobs: `syntax` (`bash -n`, `node - --check`, `python3 -m py_compile` and a PowerShell parse over every tracked - script, found by extension or `#!` line; an empty list fails the job; the - workflow scripts are checked as the async function bodies they run as, because - plain `node --check` rejects them on Node 18 and, on newer Node, reports no - error for a broken one), `suites` (every suite under `bin/tests/`, the + `ubuntu-latest` still means 24.04). Four jobs: `syntax` (`bash -n`, a + JavaScript parse, `python3 -m py_compile` and a PowerShell parse over every + tracked script, found by extension or `#!` line; an empty list fails the job; + a workflow script, any `.js` whose first statement is `export const meta`, a + skill's included, is parsed as the async function body it runs as, and any + other `.js` in explicit CommonJS, then module, mode, because plain `node + --check` rejects workflow scripts on Node 18 and, on newer Node, reports no + error for a broken one; `bin/tests/ci-gate-test.py` runs that step over every + tracked `.js` and a set of broken and valid fixtures), `suites` (every suite under `bin/tests/`, the `*-selftest` scripts and the two PowerShell suites, the destructive ones inside `sandbox-run`; the job installs bubblewrap, zsh and rsync and upgrades an older git, because `branch-gc` needs 2.45), `smoke` (a fresh install in a diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1619c57..ec56c2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,18 +76,22 @@ bash -n python3 -m py_compile # use PYTHONPYCACHEPREFIX=/tmp/pyc to keep __pycache__ out of the tree ``` -Every tracked `.js` file is a workflow script: an `export const meta` and a body -that uses top-level `await` and `return`, because the workflow runner runs the -body as an async function. Plain `node --check ` is the wrong check for -them. Node 18 rejects the valid files, and newer releases detect the `export` -and then report no error even for a file with an unclosed brace. Check the body -in the shape it runs in (error line numbers come out one higher than the -source's): +For JavaScript, run the gate's own check over every tracked `.js` file: ```bash -{ echo ';(async () => {'; sed -E -e '1{/^#!/d}' -e 's/^export[[:space:]]+//' ; echo; echo '})'; } > /tmp/check.js && node --check /tmp/check.js +python3 -B .claude-config/bin/tests/ci-gate-test.py ``` +It runs the `Check every JavaScript script` step from `gate.yml` itself, so +there is no second copy to drift. A workflow script (any `.js` whose first +statement is `export const meta =`, in `.claude-config/workflows/` or bundled +with a skill such as `SKILLS/memory-prune/audit_workflow.js`) uses top-level +`await` and `return`, because the workflow runner runs its body as an async +function, so the step parses that body as one. Plain `node --check ` is +wrong for these: Node 18 rejects the valid files, and newer releases can report +no error even for a module with an unclosed brace. Any other `.js` is checked +in explicit CommonJS, then module, mode. + **2. Every suite.** Run from your checkout root. This is the CI command; it exits non-zero if any suite fails. It needs `bwrap` (`sudo apt install bubblewrap`; see "Where destructive tests run" below) and, for the last loop, From bedaf47a36b2c1cca14be74f02229ca200b9abe1 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 11:13:05 -0400 Subject: [PATCH 18/19] docs(bin): keep one copy of the local suite command The tests section of the bin README carried its own suite loop, which printed FAILED but always exited 0 and skipped the self-tests and the PowerShell suites. It now points at step 2 of CONTRIBUTING's "Before you open the PR", the command CI mirrors, and says when ci-gate-test.py skips under the sandbox. --- .claude-config/bin/README.md | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/.claude-config/bin/README.md b/.claude-config/bin/README.md index 9fc84e4..86225d0 100644 --- a/.claude-config/bin/README.md +++ b/.claude-config/bin/README.md @@ -288,21 +288,18 @@ Runnable suites for the scripts above and for deploy's rc and core-link wiring (`deploy-rc-wiring-test.sh`, which extracts the real functions from `deploy.sh` and runs them against mktemp fixtures with a `sudo` shim, never a live rc). Each exits 0 when every case passes. A suite that starts with the -sandbox guard exits 97 outside `sandbox-run`, so run every suite through it -from the checkout root (the sandbox's own tests start `sandbox-run` and run -bare): - -```bash -cd ~/OPS -for t in .claude-config/bin/tests/*-test.sh .claude-config/bin/tests/*-test.py; do - case "$t" in *.py) run=(python3 -B) ;; *) run=(bash) ;; esac - case "$t" in */sandbox-run-test.sh|*/sandbox-run-fd-test.py) "${run[@]}" "$t" || echo "FAILED: $t"; continue ;; esac - .claude-config/bin/sandbox-run --no-bind --no-net --ro-bind "$PWD" -- "${run[@]}" "$t" || echo "FAILED: $t" -done -``` - -The older `*-selftest.sh` scripts beside their subjects (`verify-ops`, -`model-probe`, `claude-window-ping`, and the hooks) work the same way. +sandbox guard exits 97 outside `sandbox-run`, so every suite runs through it +from the checkout root, except the sandbox's own tests, which start +`sandbox-run` themselves. The command that runs them all, with the +`*-selftest` scripts beside their subjects and the PowerShell suites, and +exits non-zero if any fails, is step 2 of "Before you open the PR" in +`CONTRIBUTING.md`; CI runs the same loops (`.github/workflows/gate.yml`). +One file, one command: no second copy here to drift. + +`tests/ci-gate-test.py` runs the gate's JavaScript check over every tracked +`.js` and a set of fixtures. Under `sandbox-run` it skips when `node` lives in +your HOME (nvm, fnm), which the sandbox hides, and says so; run it bare then. +In GitHub Actions a missing `node` fails it instead. ## `grabit`: file transfer over Tailscale From 60a5d3867565a9ead3f2772a8ba4a1b8689107c7 Mon Sep 17 00:00:00 2001 From: Alex Ivantsov Date: Mon, 19 Oct 2026 11:14:23 -0400 Subject: [PATCH 19/19] docs: name the suite floor and where OPS_DIR checks fall short CONTRIBUTING's suite step now says the four branch-gc suites need git 2.45 and GNU coreutils 9.5, so a run on Ubuntu 24.04 shows why they fail. It no longer says OPS_DIR="$PWD" checks a checkout directly: the core-link check still compares the deployed rule with that checkout's core.md, so from any checkout but the deployed one it fails whatever changed. AGENTS says the same. The skills index names the brand file's real path. --- AGENTS.md | 7 ++++--- CONTRIBUTING.md | 14 ++++++++++---- SKILLS/README.md | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e248732..bb077d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,9 +28,10 @@ current task allows. - Gate: `bash .claude-config/bin/verify-ops.sh` after any harness change. It checks `~/OPS`; set `OPS_DIR` to check another checkout. Its host checks - (the core link, the Stage-1 settings) still read `$HOME`, so on a machine - with no deployed OPS run it in a throwaway `HOME` (DEPLOYMENT.md, "Testing a - deploy under a throwaway HOME"). + (the core link, the Stage-1 settings) still read `$HOME`, so for a checkout + other than the deployed one, or on a machine with no deployed OPS, run it in + a throwaway `HOME` (DEPLOYMENT.md, "Testing a deploy under a throwaway + HOME"). - Tests: `.claude-config/bin/tests/*-test.{sh,py}`, `.claude-config/bin/*-selftest.*`, and `.claude-config/hooks/*-selftest.sh`. Run the ones covering what you changed. A suite that runs a tool that deletes or moves files starts with diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec56c2e..93760f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,8 +94,10 @@ in explicit CommonJS, then module, mode. **2. Every suite.** Run from your checkout root. This is the CI command; it exits non-zero if any suite fails. It needs `bwrap` (`sudo apt install -bubblewrap`; see "Where destructive tests run" below) and, for the last loop, -`pwsh`: +bubblewrap`; see "Where destructive tests run" below), git 2.45 or newer and +GNU coreutils 9.5 or newer for the four `branch-gc` suites (Ubuntu 24.04 ships +git 2.43 and coreutils 9.4, so those suites fail there; CI runs on 26.04), and, +for the last loop, `pwsh`: ```bash fails=0 @@ -134,8 +136,12 @@ settings), so on a machine with no deployed OPS, `OPS_DIR="$PWD" .claude-config/bin/verify-ops.sh` reports a FAIL for the missing link, which says nothing about your change. Run it the way CI does, in a throwaway `HOME`: `DEPLOYMENT.md`, "Testing a deploy under a throwaway HOME" has the commands. -On a machine where OPS is already deployed, `OPS_DIR="$PWD" -.claude-config/bin/verify-ops.sh` checks your checkout directly. +On a machine where OPS is deployed, `OPS_DIR="$PWD" +.claude-config/bin/verify-ops.sh` reads your checkout's files, but its host +checks still read `$HOME`: the core-link check compares the deployed +`~/.claude/rules/ops-core.md` with your checkout's `CONTEXT/core.md`, so from +any checkout other than the deployed `~/OPS` that check FAILs whatever you +changed. The throwaway-HOME run is the clean test. **4. Secrets and private terms.** `.claude-config/bin/secrets-scan.sh CONTEXT SKILLS .claude-config` is OPS's own detector (it takes a couple of minutes: it diff --git a/SKILLS/README.md b/SKILLS/README.md index 88fa92f..8c88e52 100644 --- a/SKILLS/README.md +++ b/SKILLS/README.md @@ -98,7 +98,7 @@ Local entries live in this folder. Mounted skills are listed in the second table | `machines` | yes | no | Thin context skill: routes to the `CONTEXT/machines.md` slot for host topology; degrades (OPS ships no machines.md) to asking or inferring. | | `harness-deploy` | yes | no | Thin context skill: routes to `DEPLOYMENT.md` when changing Stage 1 / Stage 2 install. | | `harness-readme` | yes | no | Thin context skill: routes to `README.md` for "what exists / where does X live". | -| `design-method` | yes | no | Design method (UI, web, landing pages, email templates) with an optional brand layer read from `design/brand.md`; free-design until the brand file is filled. | +| `design-method` | yes | no | Design method (UI, web, landing pages, email templates) with an optional brand layer read from `design-method/brand.md`; free-design until the brand file is filled. | Mounted from [`Exploitacious/agent-skills`](https://github.com/Exploitacious/agent-skills) (public, MIT) through `VENDORED.tsv`. Edit these at the source, then sync; never in `SKILLS/`.