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 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/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/.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-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', 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/.github/workflows/gate.yml b/.github/workflows/gate.yml new file mode 100644 index 0000000..9c6376f --- /dev/null +++ b/.github/workflows/gate.yml @@ -0,0 +1,357 @@ +# The OPS gate. Runs on every push and pull request: +# +# 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 +# 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). CONTRIBUTING.md ("Before you open the +# PR") lists 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' + # 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 + 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: 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 + 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" + + - 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-26.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, 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. 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 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 + command -v "${pkg#*:}" >/dev/null 2>&1 || need+=("${pkg%%:*}") + done + 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 + [ "${#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 + echo "bwrap is blocked here; lifting the AppArmor user-namespace restriction" + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + fi + 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 + # 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-26.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-26.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 diff --git a/AGENTS.md b/AGENTS.md index 4c0789e..bb077d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,14 +27,22 @@ 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 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 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`. + 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. - `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 @@ -43,9 +51,12 @@ 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. + [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 +96,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/CHANGELOG.md b/CHANGELOG.md index 758ee1f..ea18c49 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,78 @@ 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`, 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` + (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` + 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 + 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 + 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 + 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, 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`, 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 + 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/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/CONTRIBUTING.md b/CONTRIBUTING.md index c28147f..93760f6 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,147 @@ 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 +bash -n +python3 -m py_compile # use PYTHONPYCACHEPREFIX=/tmp/pyc to keep __pycache__ out of the tree +``` + +For JavaScript, run the gate's own check over every tracked `.js` file: + +```bash +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), 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 -.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/") +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 +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 -bash -n -node --check +echo "failed suites: $fails" +[ "$fails" -eq 0 ] # last, so the exit status is the result (the echo exits 0) ``` -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 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 +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. 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. 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/DEPLOYMENT.md b/DEPLOYMENT.md index 8d7c93a..0b0ed8a 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 @@ -194,10 +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 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`, 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 @@ -245,8 +248,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 +297,65 @@ 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 + 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 +) +``` + +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. + +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 @@ -336,7 +385,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 +615,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/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/README.md b/README.md index fafcd79..5fd42ac 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,32 @@ 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). 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. +- **`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: @@ -107,7 +135,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 @@ -130,6 +158,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 +192,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 +346,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 +357,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 +429,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). 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/`. 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 +``` 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.