From d8ec72e253691e4faceb751fbbfef086e52b3f6d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 18:06:23 -0300 Subject: [PATCH 01/24] Harden eternal stack gates and policy contracts --- .github/workflows/health.yml | 52 ++ .gitignore | 3 + CHANGELOG.md | 34 +- CREDITS.md | 4 +- README.md | 2 +- VERSION | 2 +- commands/email-triage.md | 20 +- docs/adr/0003-exodia-cross-host-rules.md | 12 +- docs/eternal-stack-coverage.md | 8 +- docs/health-stack.md | 2 + docs/install.md | 4 +- docs/rules.md | 8 +- docs/skills.md | 8 +- hooks/cc-posttoolbatch-observer.sh | 6 +- hooks/cc-posttoolusefailure-diagnose.sh | 2 +- hooks/cc-pretooluse-guard.sh | 57 +- hooks/cc-stop-verifier.sh | 28 +- hooks/cc-userprompt-router.sh | 6 +- rules-manifest.json | 497 +++++++++++++++++- rules/eternal-saas/global/00-stack.md | 2 +- rules/eternal-saas/global/10-gotchas.md | 2 +- rules/eternal-saas/global/20-verify.md | 2 +- rules/eternal-saas/project/auth-tenancy.md | 2 +- rules/eternal-saas/project/components.md | 8 +- rules/eternal-saas/project/deploy-onveloz.md | 2 +- rules/eternal-saas/project/i18n.md | 2 +- rules/eternal-saas/project/imports.md | 2 +- rules/eternal-saas/project/local-overrides.md | 2 +- rules/eternal-saas/project/money.md | 2 +- rules/eternal-saas/project/next.md | 2 +- rules/eternal-saas/project/orpc.md | 2 +- rules/eternal-saas/project/oxlint.md | 2 +- rules/eternal-saas/project/prisma.md | 2 +- rules/eternal-saas/project/react.md | 2 +- rules/eternal-saas/project/stack.md | 2 +- rules/eternal-saas/project/tcg-contract.md | 41 ++ rules/eternal-saas/project/testing.md | 2 +- rules/eternal-saas/project/typescript.md | 2 +- rules/etrnl/domains.md | 4 +- scripts/bootstrap-tools.sh | 14 +- scripts/changelog-release-check.mjs | 10 +- scripts/code-health-ledger-check.mjs | 7 + scripts/deep-audit-artifact-check.mjs | 6 +- scripts/doctor.sh | 87 ++- scripts/init-project-rules.sh | 250 ++++++--- scripts/install.sh | 5 +- scripts/performance-baseline.mjs | 6 +- scripts/plan-readiness-check.mjs | 2 +- scripts/post-upgrade-canary.sh | 28 +- scripts/pr-preflight.mjs | 1 + scripts/skill-contract-check.mjs | 48 +- scripts/skill-update-prompt.mjs | 28 +- scripts/sync-rule-exports.mjs | 250 +++++++-- scripts/tool-stack-check.mjs | 12 +- scripts/update-check.mjs | 10 +- .../bundled/abacatepay-integration/SKILL.md | 2 +- skills/bundled/better-auth/SKILL.md | 17 +- skills/bundled/brooks-audit/SKILL.md | 2 +- .../brooks-audit/architecture-guide.md | 2 +- .../bundled/brooks-audit/onboarding-guide.md | 4 +- skills/bundled/ci-cd/SKILL.md | 26 +- skills/bundled/ci-cd/evals/prompts.md | 2 +- .../bundled/ci-cd/references/deep-playbook.md | 8 +- skills/bundled/ci-cd/references/source-map.md | 2 +- skills/bundled/code-simplifier/SKILL.md | 10 +- skills/bundled/domain-ml/SKILL.md | 2 +- .../bundled/eternal-best-practices/SKILL.md | 75 +-- .../finding-duplicate-functions/SKILL.md | 2 +- .../scripts/find-duplicates-prompt.md | 4 +- .../scripts/generate-report.sh | 8 +- skills/bundled/i18n-localization/SKILL.md | 4 +- skills/bundled/money-vo-discipline/SKILL.md | 16 +- skills/bundled/orpc-patterns/SKILL.md | 8 +- .../bundled/orpc-patterns/agents/openai.yaml | 4 +- .../orpc-patterns/references/client-links.md | 4 +- .../orpc-patterns/references/contracts.md | 2 +- .../references/getting-started.md | 6 +- .../references/integrations-observability.md | 2 +- .../references/middleware-context.md | 2 +- .../references/migrations-cookbook.md | 4 +- .../references/plugins-security.md | 2 +- .../orpc-patterns/references/procedures.md | 8 +- .../streaming-files-serialization.md | 6 +- .../references/tanstack-query.md | 4 +- .../references/testing-monorepo.md | 4 +- skills/bundled/prisma-expert/SKILL.md | 2 +- .../sql-optimization-patterns/SKILL.md | 4 +- skills/bundled/stripe-best-practices/SKILL.md | 22 +- .../tenant-isolation-patterns/SKILL.md | 6 +- templates/AGENTS.override.codex.md | 2 +- .../project/eternal-saas-components.mdc | 6 +- .../project/eternal-saas-tcg-contract.mdc | 29 + tests/test-hooks.sh | 96 ++-- tests/test-workflow-tools.sh | 120 ++++- 94 files changed, 1650 insertions(+), 485 deletions(-) create mode 100644 .github/workflows/health.yml create mode 100644 rules/eternal-saas/project/tcg-contract.md mode change 100644 => 100755 scripts/sync-rule-exports.mjs create mode 100644 templates/cursor/rules/eternal-saas/project/eternal-saas-tcg-contract.mdc diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml new file mode 100644 index 0000000..59cb790 --- /dev/null +++ b/.github/workflows/health.yml @@ -0,0 +1,52 @@ +name: health + +on: + pull_request: + push: + branches: + - main + - "release/**" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + repo-health: + name: Repository Health Checks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + + - name: Set up Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: "22" + cache: "" + + - name: Install CLI dependencies + run: | + sudo apt-get update + sudo apt-get install -y jq fd-find ripgrep shellcheck + sudo ln -sf "$(command -v fdfind)" /usr/local/bin/fd + + - name: Validate generated rule exports + run: node scripts/sync-rule-exports.mjs --check + + - name: Hook tests + run: tests/test-hooks.sh + + - name: Workflow tool tests + run: tests/test-workflow-tools.sh + + - name: Install and rollback tests + run: tests/test-install.sh + + - name: Doctor + run: scripts/doctor.sh --jobs 4 diff --git a/.gitignore b/.gitignore index 96ee22f..124e151 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ coverage/ plans/ docs/plans/ docs/research/ +# Local privacy overlay is intentionally ignored without hiding all .eternal/ state. +.eternal/privacy-banned-tokens.local +rules-manifest.local.json # Never commit private Claude runtime data. settings.local.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b31cd4a..f257be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this ### Deprecated +## v0.5.2 + +2026-06-16 + +### Added + +- `.github/workflows/health.yml` — CI health workflow for rule export sync, hook tests, workflow tests, install/rollback tests, and doctor. +- `rules/eternal-saas/project/tcg-contract.md` — scoped TCG/card-domain contract rule module and generated Cursor export. + +### Changed + +- `scripts/sync-rule-exports.mjs` and `scripts/init-project-rules.sh` — manifest-driven rule sync now validates profile membership, generated Cursor exports, privacy overlays, and install-time Cursor checksums. +- `rules/eternal-saas/*` — rule host metadata now reflects Claude and Cursor support without claiming unsupported Codex nested context output. +- `skills/bundled/stripe-best-practices` — hardens Stripe guidance from advisory wording to explicit policy gates for API versions, payment-surface selection, test/migration expectations, and Connect settlement/dispute behavior. + +### Fixed + +- `scripts/install.sh` — validate source install inputs before any non-dry-run mutation. +- `hooks/cc-stop-verifier.sh`, `hooks/cc-pretooluse-guard.sh`, and `scripts/code-health-ledger-check.mjs` — close enforcement gaps for invalid Stop JSON, live hook writes, and prompt-only code-health audits. +- `scripts/update-check.mjs`, `scripts/skill-contract-check.mjs`, `scripts/tool-stack-check.mjs`, and `scripts/doctor.sh` — harden update trust, bundled skill contracts, pinned tool install specs, ShellCheck, privacy scanning, and rule export drift detection. + +### Security + +- `rules-manifest.json` and `scripts/doctor.sh` — remove tracked private project literals from the privacy gate and support gitignored local banned-token overlays with redacted diagnostics. + ## v0.5.1 2026-06-11 @@ -53,14 +78,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this - `templates/AGENTS.global.md` — portable ~32-line cross-host agent baseline for Codex startup. - `templates/AGENTS.override.codex.md` — Codex-specific startup deltas (no slash commands, no hooks, byte budget, skills path). - `docs/rules.md` — cross-host rules reference: module catalog, host activation per tool, install and drift-check commands. -- `docs/adr/0003-cross-host-rule-stack.md` — decision record for the Exodia cross-host rule architecture. +- `docs/adr/0003-exodia-cross-host-rules.md` — decision record for the Exodia cross-host rule architecture. - Codex byte gate in `scripts/doctor.sh` — warns when `~/.codex/AGENTS.md` exceeds 75 % of the configured `project_doc_max_bytes` limit. - Manifest assertions in `scripts/doctor.sh` — validates `rules-manifest.json` schema version, `bannedTokens` non-empty, and `rules/eternal-saas/global/` module count. - Rollback now restores `rules/eternal-saas` global digest and backed-up Codex startup files (`AGENTS.md`, `AGENTS.override.md`). - `scripts/lib/skill-lists.sh` now includes `init-project-rules.sh` in `INSTALL_SCRIPTS` so it deploys to both Claude and Codex homes. - Prompt router extended: "prune AGENTS/claude/rules", "rule bloat", "AGENTS.md/CLAUDE.md too long", "trim AGENTS/CLAUDE.md", and "startup file/context too long" prompts now route to `etrnl-ops-agent-files`. Three new skill-triggering fixture cases added. -- Six project pilots with the eternal-saas pack: core-suite, agency-tbd, tcg-collector, mimo-finance, vivaz-website, and sbcc-portal — each with project-specific `local-overrides.md`, pruned `AGENTS.md`, and removed old flat rule files. -- sbcc-portal `.gitignore` updated to track `.claude/rules/` while keeping local session state ignored. +- Six private project pilots with the eternal-saas pack, each with project-specific `local-overrides.md`, pruned `AGENTS.md`, and removed old flat rule files. +- One private pilot `.gitignore` updated to track `.claude/rules/` while keeping local session state ignored. ### Changed @@ -73,7 +98,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this ### Fixed -- `scripts/plan-readiness-check.mjs` no longer flags hyphenated proper names such as the `agency-tbd` repo as a `TBD` placeholder; standalone `TBD` markers still fail (regression tests in `tests/test-workflow-tools.sh`). +- `scripts/plan-readiness-check.mjs` no longer flags hyphenated proper names such as the `example-agency` repo as a `TBD` placeholder; standalone `TBD` markers still fail (regression tests in `tests/test-workflow-tools.sh`). - `scripts/update-check.mjs` now correctly marks `sync-rule-exports.mjs` as source-only (not installed) to prevent false drift failures. - `scripts/update-check.mjs` renamed map includes `doctor.sh → doctor-etrnl.sh` to suppress stale-scripts drift false positives. @@ -94,4 +119,3 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this ### Security - Public repository boundary: no private identity, credentials, transcripts, or local planning artifacts in tracked files. - diff --git a/CREDITS.md b/CREDITS.md index 1aff19c..3e4893f 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -28,7 +28,9 @@ Eternal Stack is designed as a complete skill family. Policy, review, simplifica | `better-auth`, `tenant-isolation-patterns`, `money-vo-discipline` | Auth, tenancy, and money discipline | Community / upstream skill bundles | | `stripe-best-practices`, `abacatepay-integration` | Payments review | Community / upstream skill bundles | | `ci-cd` | CI helper scripts referenced by `/etrnl-dev-ci` | Community skill bundle | -| `domain-*`, `i18n-localization`, and related domain skills | Domain-specific review gates | Community / upstream skill bundles | +| `domain-cli`, `domain-cloud-native`, `domain-embedded`, `domain-fintech`, `domain-iot`, `domain-ml`, `domain-web` | Domain-specific review gates | Community / upstream skill bundles | +| `prisma-expert` | Prisma schema and query review | Community / upstream skill bundle | +| `i18n-localization` | Locale and translation review | Community / upstream skill bundle | The full inventory and routing notes live in [docs/skills.md](docs/skills.md). diff --git a/README.md b/README.md index cf73c8b..436b32a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ tests/test-hooks.sh **Hooks** — enforcement at tool boundaries. Full catalog and lifecycle wiring: [docs/hooks.md](docs/hooks.md). Pretool and stop rules: [docs/guards.md](docs/guards.md). Regression: [tests/test-hooks.sh](tests/test-hooks.sh). -**Skills** — repeatable workflows as `/etrnl-*` commands, grouped by namespace (`dev`, `audit`, `ops`, `comm`). Inventory: [docs/skills.md](docs/skills.md). +**Skills** — repeatable workflows as `/etrnl-*` commands, grouped by namespace (`dev`, `audit`, `ops`, `comm`). Bundled domain skills include `domain-cli`, `domain-cloud-native`, `domain-embedded`, `domain-fintech`, `domain-iot`, `domain-ml`, and `domain-web`. Inventory: [docs/skills.md](docs/skills.md). **Scripts** — deterministic helpers for ledgers, browser QA, workflow health, code-health inventory, deep-audit validation, and release hygiene. diff --git a/VERSION b/VERSION index 4b9fcbe..cb0c939 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.1 +0.5.2 diff --git a/commands/email-triage.md b/commands/email-triage.md index bb604e7..9b866d5 100644 --- a/commands/email-triage.md +++ b/commands/email-triage.md @@ -1,5 +1,5 @@ --- -description: Run VIVAZ email Inbox Zero triage for one account, then open the action queue. +description: Run managed email Inbox Zero triage for one account, then open the action queue. argument-hint: allowed-tools: Bash --- @@ -8,19 +8,19 @@ Account argument from the slash command: `$ARGUMENTS` Treat the argument as the account id. If it is empty, ask the repository owner for the account id and stop. -Do not handwrite Gmail commands. Do not send email. Do not mutate Gmail outside the VIVAZ email runtime. -Do not run `vivaz-email triage run` for this slash command. That is a dry classification path and does not clear INBOX. +Do not handwrite Gmail commands. Do not send email. Do not mutate Gmail outside the managed email runtime. +Do not run `etrnl-email triage run` for this slash command. That is a dry classification path and does not clear INBOX. Phase 1 is Inbox Zero. Triage every email in INBOX, archive known bad-quality emails, label action/waiting/manual-review items, remove them from INBOX, and provider-verify INBOX is zero: ```bash -vivaz-email triage guarded-run --account --max-inbox 500 --apply --require-insights +etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights ``` Verify the queue run before opening any queue: ```bash -vivaz-email triage verify --latest --account +etrnl-email triage verify --latest --account ``` If verification does not show `inbox_zero_verified: true`, `inbox_count: 0`, and either `gmail_mutated: true` or `queue_ready_without_mutation: true`, do not show queue items. Continue Inbox Zero triage first or paste the runtime blocker. @@ -28,21 +28,21 @@ If verification does not show `inbox_zero_verified: true`, `inbox_count: 0`, and If `guarded-run` exits with `TRIAGE_GUARD_ML_DISAGREED`, do not ask the repository owner whether to continue. Inspect the runtime evidence, patch deterministic triage rules/cache when appropriate, then rerun the guarded command: ```bash -vivaz-email triage guarded-run --account --max-inbox 500 --apply --require-insights -vivaz-email triage ml-reviews --latest --account --limit 20 -vivaz-email triage report --latest --account --include-failures --format markdown +etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights +etrnl-email triage ml-reviews --latest --account --limit 20 +etrnl-email triage report --latest --account --include-failures --format markdown ``` Phase 2 starts only after Inbox Zero is verified. Use the queue run id emitted by the runtime, then show exactly one action/reply queue item: ```bash -vivaz-email triage queue --run-id --mode reply --format markdown --next +etrnl-email triage queue --run-id --mode reply --format markdown --next ``` If the queue item shows a proposed reply with a draft id, run the outgoing reply checker before asking the repository owner to approve or send it: ```bash -vivaz-email drafts check --draft-id +etrnl-email drafts check --draft-id ``` If the checker returns any issue, stop and surface the failed draft check with the exact issue list. Do not improvise manual rewrites, and do not ask the repository owner to approve or send a failed draft until the runtime provides a checked replacement draft. diff --git a/docs/adr/0003-exodia-cross-host-rules.md b/docs/adr/0003-exodia-cross-host-rules.md index 4f65334..4f7c1ef 100644 --- a/docs/adr/0003-exodia-cross-host-rules.md +++ b/docs/adr/0003-exodia-cross-host-rules.md @@ -7,11 +7,11 @@ date: 2026-06-10 ## Context -Claude Code, Codex, and Cursor each provide native agent-context surfaces (`.claude/rules/` with `paths:` frontmatter, `AGENTS.md` nesting, and `.mdc` with `globs`), but the rule content is currently authored separately for each project. This creates drift, duplication, and inconsistent enforcement surfaces across repos. +Claude Code, Codex, and Cursor each provide native agent-context surfaces (`.claude/rules/` with `paths:` frontmatter, global `AGENTS.md` startup context, and `.mdc` with `globs`), but the rule content is currently authored separately for each project. This creates drift, duplication, and inconsistent enforcement surfaces across repos. Host features verified on 2026-06-10: - **Claude Code** natively loads `.claude/rules/` and `~/.claude/rules/` with `paths:` frontmatter scoping; no hooks needed. -- **Codex** reads `~/.codex/AGENTS.md` and `AGENTS.override.md`; nested `AGENTS.md` files are its only depth mechanism; no glob or import syntax. +- **Codex** reads `~/.codex/AGENTS.md` and `AGENTS.override.md`; this repo installs the global startup digest and does not install project-depth rule modules for Codex. - **Cursor** `.mdc` files with `globs`, `description`, and `alwaysApply` are native; Cursor has no user-level rules directory (settings UI only). `scripts/install.sh` already syncs `rules/etrnl` to `~/.claude/rules/etrnl` with an atomic tmp/old swap and implements `ETRNL_INSTALL_STARTUP` gating for startup files. The bundled skill family already publishes Eternal-stack patterns publicly (`money-vo-discipline`, `abacatepay-integration`, `eternal-best-practices`). @@ -24,11 +24,11 @@ This record is ADR 0003. ADR 0002 is taken by `etrnl-state-and-compact-handoff`. ### 2. Privacy boundary -The `eternal-saas` rule pack ships publicly. Excluded from tracked rule files: client business names, account facts, credentials, transcripts, and personal identity. Client-repo rollout lists stay in local gitignored planning paths. Enforcement: `rules-manifest.json` carries `privacy.bannedTokens`; `sync-rule-exports.mjs --check` fails when a tracked rule file contains one. +The `eternal-saas` rule pack ships publicly. Excluded from tracked rule files: client business names, account facts, credentials, transcripts, and personal identity. Client-repo rollout lists stay in local gitignored planning paths. Enforcement: `rules-manifest.json` carries generic privacy sentinel tokens plus optional untracked local token files; `sync-rule-exports.mjs --check` fails when a tracked rule file contains one. -### 3. Codex scoped depth via nested AGENTS.md +### 3. Codex uses the startup digest -Each rule module may declare `codexNested: `; `sync-rule-exports.mjs` emits a nested `AGENTS.md` for declared modules; undeclared modules ride the root digest only. No import syntax exists in Codex — `@` imports are never used in Codex files. +The project rule pack installs to Claude Code and Cursor project surfaces. Codex receives the shared baseline through `~/.codex/AGENTS.md` and `AGENTS.override.md`; no nested project `AGENTS.md` files are generated by `sync-rule-exports.mjs` or `init-project-rules.sh`. ### 4. Byte budget is read, not assumed @@ -48,7 +48,7 @@ All project-pack installs use file copies. Symlinks break for other clones, CI, ## Consequences -- One source of truth for rule content; Claude `.claude/rules/`, Cursor `.mdc`, and Codex `AGENTS.md` files are generated or installed from the same module. +- One source of truth for project rule content; Claude `.claude/rules/` and Cursor `.mdc` files are generated or installed from the same module. Codex receives the global baseline through startup files. - `sync-rule-exports.mjs --check` in the test suite prevents host-twin drift and banned-token leaks. - The byte-gate in `doctor.sh` keeps Codex context under the effective limit; the explicit fallback prevents silent overflow. - Checksum-tracked installs let pilot repos self-classify drift without re-running install. diff --git a/docs/eternal-stack-coverage.md b/docs/eternal-stack-coverage.md index 070cec0..1356402 100644 --- a/docs/eternal-stack-coverage.md +++ b/docs/eternal-stack-coverage.md @@ -27,7 +27,13 @@ Status key: `done` means implemented in this repo; `live-gated` means intentiona Eternal Stack is a bundled skill family: `etrnl-*` orchestration from this repo plus policy, review, and domain skills that install on the host and are routed by hooks and workflows. See `docs/skills.md` for the full inventory. Representative bundled skills: - `eternal-best-practices` -- `domain-*` +- `domain-cli` +- `domain-cloud-native` +- `domain-embedded` +- `domain-fintech` +- `domain-iot` +- `domain-ml` +- `domain-web` - `better-auth` - `tenant-isolation-patterns` - `money-vo-discipline` diff --git a/docs/health-stack.md b/docs/health-stack.md index 4f59bc0..346ee34 100644 --- a/docs/health-stack.md +++ b/docs/health-stack.md @@ -126,6 +126,8 @@ node scripts/update-check.mjs --explain scripts/post-upgrade-canary.sh ``` +- `.github/workflows/health.yml` runs the repository health pipeline in GitHub Actions on every pull request, on pushes to `main`, and on pushes to `release/**` branches. The workflow validates generated rule exports, then runs `tests/test-hooks.sh`, `tests/test-workflow-tools.sh`, `tests/test-install.sh`, and `scripts/doctor.sh --jobs 4`. +- The workflow is the hosted counterpart to the Required Gates block above: `sync-rule-exports.mjs --check` covers generated rule drift, the hook/workflow/install suites cover runtime behavior and rollback safety, and `scripts/doctor.sh --jobs 4` replays the aggregated syntax, ShellCheck, manifest, privacy, documentation, and heavy-suite health checks. - `scripts/workflow-health.mjs` reads run ledgers in parallel with `ETRNL_LEDGER_READ_CONCURRENCY` (default `8`, capped at `12` for constrained systems). `workflow-health.mjs status` is the concise text surface used by SessionStart hints; `status --json` is the machine-readable surface for active run id, unfinished work, missing artifacts, browser/context freshness, phase/UAT state, stale run count, and the next deterministic action. Use `workflow-health.mjs doctor --strict` or `ETRNL_WORKFLOW_HEALTH_STRICT=1` when live runtime findings must fail closed instead of remaining diagnostic. - `tool-effectiveness.mjs` summarizes sanitized local tool events into deterministic `keep`, `enforce`, `repo-specific`, `remove-watch`, or `insufficient-data` verdicts. It reads hook tool-signal state, optional local event artifacts, and explicit Codex imports; it rejects raw prompts, transcript text, secrets, private transcript paths, and tracked private project names. Use the seven-day `summarize` command above to revisit CodeGraph, Beads, and stolen hook patterns without manual log reading. - `etrnl-state.mjs` is the canonical local state helper for compact lifecycle and small workflow events. It writes append-only JSONL under `~/.claude/etrnl/state`, rebuilds compact handoff views, rejects raw prompts/transcripts/private paths/secrets before append, and exposes `compact-handoff`, `stop-status`, `doctor`, `bead-link`, and `bead-prime-audit`. Hook hot paths may use bounded state appends and queries only. diff --git a/docs/install.md b/docs/install.md index 859872d..9be93b7 100644 --- a/docs/install.md +++ b/docs/install.md @@ -7,6 +7,8 @@ Public home: [github.com/eternalwaitt/eternal-stack](https://github.com/eternalw ./scripts/doctor.sh ``` +Prerequisites: `bash`, `git`, `node`, `python3`, `jq`, `rg`, `fd`, `sg`, and `shellcheck` must be available on PATH. `python3` is required by rule receipt parsing, project-rule installation checks, Hindsight hook syntax checks, and the doctor privacy scan. + Default install is intentionally usable but conservative: `--profile core` installs observer hooks, prompt routing, prompt expansion, once-per-session `CLAUDE.md` reinjection, the locked advisory rate limiter, post-tool observation, session cleanup, scripts, docs, rules, skills, and agents. Hard blockers and global memory/backlog/codegraph services stay opt-in. Breaking install behavior: managed `~/.claude/settings.json` is backed up and reset to a vanilla settings shell before the stack is applied unless `--preserve-settings` is supplied. Existing `enabledPlugins` entries are preserved so installs do not disable already-enabled Claude Code plugins. Live migration of memory systems, plugins, MCPs, broad permissions, and private overlays is a separate local rollout step, not an automatic install-time side effect. @@ -39,7 +41,7 @@ The installer: - public `AGENTS.md` baseline - tiny `CLAUDE.md` wrapper - namespaced rules and cross-host rule pack (`rules/eternal-saas/global/` + `rules/eternal-saas/project/`) - - `init-project-rules.sh` — installs the rule pack into a target project for Claude, Codex, and Cursor + - `init-project-rules.sh` — installs the rule pack into a target project for Claude and Cursor - rollback script - canaries - hook test harness diff --git a/docs/rules.md b/docs/rules.md index 1aaf61f..6589156 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -7,7 +7,7 @@ Rule modules live in `rules/` and ship as a cross-host pack. Each module is a fo | Host | Mechanism | Scope | | --- | --- | --- | | **Claude Code** | `.claude/rules/` in project or `~/.claude/rules/` globally; `paths:` frontmatter scopes to matched paths | Path-scoped or global | -| **Codex** | `~/.codex/AGENTS.md` (global digest); nested `AGENTS.md` for depth (declared via `codexNested:` in manifest) | Digest in global; modules in nested | +| **Codex** | `~/.codex/AGENTS.md` and `~/.codex/AGENTS.override.md` installed by `scripts/install.sh` | Global startup digest | | **Cursor** | `.cursor/rules/*.mdc` with `globs:` and `description:` frontmatter | Glob-matched per file | Cursor has no user-level rules directory — project `.mdc` files are the only automated surface. Global rules require manual copy to each project. @@ -48,11 +48,11 @@ node scripts/sync-rule-exports.mjs --check ## Manifest -`rules-manifest.json` at the repo root declares profiles, module metadata, and privacy `bannedTokens`. The `modules:` object is populated by `sync-rule-exports.mjs`. Schema version 1. +`rules-manifest.json` at the repo root declares profiles, module metadata, and the generic privacy sentinel tokens used by the export check. The `modules:` object is populated by `sync-rule-exports.mjs`. Schema version 1. ## Privacy gate -`sync-rule-exports.mjs --check` fails when any tracked rule file contains a token from `privacy.bannedTokens`. Client repo names, account facts, credentials, and personal identity must stay in local gitignored overlays, never in tracked rule files. +`sync-rule-exports.mjs --check` fails when any tracked rule file contains a generic privacy sentinel token or a token from the optional untracked local privacy files. Keep tracked sentinels generic; client repo names, account facts, credentials, and personal identity belong only in local gitignored overlays, never in tracked files. ## Module authoring @@ -66,7 +66,7 @@ paths: globs: - "apps/web/src/lib/**" description: "One-line description for context matching." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm guard:essential" --- ``` diff --git a/docs/skills.md b/docs/skills.md index 9a024fc..6afa91b 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -156,7 +156,13 @@ Beads is not an ETRNL bundled execution skill. It is allowed as explicit backlog | Skill | Bundle role | Used for | | --- | --- | --- | | `eternal-best-practices` | Bundled policy | Auth, tenant isolation, money, i18n, Prisma, soft deletes, and domain-sensitive work. | -| `domain-*` | Bundled domain | Cloud, web, fintech, IoT, embedded, ML, and similar review gates. | +| `domain-cli` | Bundled domain | CLI application review gates. | +| `domain-cloud-native` | Bundled domain | Cloud-native architecture and operations review gates. | +| `domain-embedded` | Bundled domain | Embedded systems review gates. | +| `domain-fintech` | Bundled domain | Fintech product and integration review gates. | +| `domain-iot` | Bundled domain | IoT systems review gates. | +| `domain-ml` | Bundled domain | ML system review gates. | +| `domain-web` | Bundled domain | Web application review gates. | | `better-auth` | Bundled auth | Auth implementation review on protected auth paths. | | `tenant-isolation-patterns` | Bundled tenancy | Multi-tenant data and permission boundaries. | | `money-vo-discipline` | Bundled finance | Money/value-object discipline on financial and billing paths. | diff --git a/hooks/cc-posttoolbatch-observer.sh b/hooks/cc-posttoolbatch-observer.sh index f761f9a..2cfc94e 100755 --- a/hooks/cc-posttoolbatch-observer.sh +++ b/hooks/cc-posttoolbatch-observer.sh @@ -294,9 +294,9 @@ record_tool() { if cc_command_is_review_verification "$command"; then cc_state_batch_append_value reviewRuns "$command" fi - local vivaz_triage_regex='(^|[[:space:];&|])(vivaz-email|[^[:space:];&|]*/vivaz-email)[[:space:]]+triage[[:space:]]+(verify|report)([[:space:]]|$)' - # Match standalone or path-prefixed `vivaz-email triage verify` or `report`; anchors avoid partial command-word matches. - if [[ "$command" =~ $vivaz_triage_regex ]]; then + local email_triage_regex='(^|[[:space:];&|])([^[:space:];&|]*/)?etrnl-email[[:space:]]+triage[[:space:]]+(verify|report)([[:space:]]|$)' + # Match standalone or path-prefixed `etrnl-email triage verify` or `report`; anchors avoid partial command-word matches. + if [[ "$command" =~ $email_triage_regex ]]; then cc_state_batch_append_value verificationRuns "$command" fi ;; diff --git a/hooks/cc-posttoolusefailure-diagnose.sh b/hooks/cc-posttoolusefailure-diagnose.sh index b9a53a4..bc83ab3 100755 --- a/hooks/cc-posttoolusefailure-diagnose.sh +++ b/hooks/cc-posttoolusefailure-diagnose.sh @@ -53,7 +53,7 @@ failure_hint() { return 0 ;; *"triage_guard_ml_disagreed"*|*"ml archive review found"*disagreement*) - printf 'Email-triage ML disagreement is a recoverable guard path, not a question for the repository owner. Inspect the run with vivaz-email triage ml-reviews --latest --account --limit 20, then patch deterministic rules/cache or rerun guarded-run with the exact run id from the runtime output.' + printf 'Email-triage ML disagreement is a recoverable guard path, not a question for the repository owner. Inspect the run with etrnl-email triage ml-reviews --latest --account --limit 20, then patch deterministic rules/cache or rerun guarded-run with the exact run id from the runtime output.' return 0 ;; *"veloz deploy"*|*"vercel deploy"*) diff --git a/hooks/cc-pretooluse-guard.sh b/hooks/cc-pretooluse-guard.sh index 2bec498..41e0871 100755 --- a/hooks/cc-pretooluse-guard.sh +++ b/hooks/cc-pretooluse-guard.sh @@ -207,20 +207,20 @@ cc_disk_cleanup_active() { command_is_raw_email_triage_gmail_mutation() { local cmd="$1" - local vivaz_apply_regex='(^|[[:space:];&|])([^[:space:];&|]*/)?vivaz-email[[:space:]]+triage[[:space:]]+apply([[:space:]]|$)' + local email_apply_regex='(^|[[:space:];&|])([^[:space:];&|]*/)?etrnl-email[[:space:]]+triage[[:space:]]+apply([[:space:]]|$)' local gmail_mutation_regex='(gws|gmail)[^;&|]*(batchModify|modify|move|trash|delete)' - [[ "$cmd" =~ $vivaz_apply_regex ]] && return 1 + [[ "$cmd" =~ $email_apply_regex ]] && return 1 [[ "$cmd" =~ $gmail_mutation_regex ]] } command_is_email_triage_queue() { local cmd="$1" - [[ "$cmd" =~ (^|[[:space:];&|])([^[:space:];&|]*/)?vivaz-email[[:space:]]+triage[[:space:]]+queue([[:space:]]|$) ]] + [[ "$cmd" =~ (^|[[:space:];&|])([^[:space:];&|]*/)?etrnl-email[[:space:]]+triage[[:space:]]+queue([[:space:]]|$) ]] } command_is_email_triage_dry_run() { local cmd="$1" - [[ "$cmd" =~ (^|[[:space:];&|])([^[:space:];&|]*/)?vivaz-email[[:space:]]+triage[[:space:]]+run([[:space:]]|$) ]] + [[ "$cmd" =~ (^|[[:space:];&|])([^[:space:];&|]*/)?etrnl-email[[:space:]]+triage[[:space:]]+run([[:space:]]|$) ]] } command_is_email_triage_debug_dry_run() { @@ -239,7 +239,7 @@ cc_email_triage_verify_seen() { email_triage_request_at as $since | any(.successfulCommands[]?; ((.at // "") >= $since) - and ((.command // "") | test("(^|[[:space:];&|])([^[:space:];&|]*/)?vivaz-email[[:space:]]+triage[[:space:]]+verify([[:space:]]|$)"))) + and ((.command // "") | test("(^|[[:space:];&|])([^[:space:];&|]*/)?etrnl-email[[:space:]]+triage[[:space:]]+verify([[:space:]]|$)"))) ' "$(cc_state_file)" >/dev/null 2>&1 } @@ -268,7 +268,7 @@ cc_email_triage_latest_account_after() { [.successfulCommands[]? | select((.at // "") >= $since) | (.command // "") - | select(test("(^|[[:space:];&|])([^[:space:];&|]*/)?vivaz-email[[:space:]]+triage[[:space:]]+(run|guarded-run)([[:space:]]|$)"))] + | select(test("(^|[[:space:];&|])([^[:space:];&|]*/)?etrnl-email[[:space:]]+triage[[:space:]]+(run|guarded-run)([[:space:]]|$)"))] | last // "" ' "$(cc_state_file)" 2>/dev/null)" account="" @@ -280,7 +280,7 @@ cc_email_triage_latest_account_after() { cc_email_triage_cli() { local candidate resolved - for candidate in "${VIVAZ_EMAIL_BIN:-}" "${VIVAZ_EMAIL_CLI:-}"; do + for candidate in "${ETRNL_EMAIL_BIN:-}" "${ETRNL_EMAIL_CLI:-}"; do [[ -n "$candidate" ]] || continue if [[ -x "$candidate" ]]; then printf '%s\n' "$candidate" @@ -291,8 +291,8 @@ cc_email_triage_cli() { return 0 fi done - if command -v vivaz-email >/dev/null 2>&1; then - command -v vivaz-email + if command -v etrnl-email >/dev/null 2>&1; then + command -v etrnl-email return 0 fi return 1 @@ -329,8 +329,25 @@ cc_email_triage_queue_verified() { command_writes_live_claude_hooks() { local cmd="$1" - local live_hook_write_re="(tee|cat|cp|mv|rsync|install|chmod|chown|rm|trash)[^;&|]*((\\\$HOME|~|/Users/[^[:space:]/]+)?/\\.claude/hooks)" - [[ "$cmd" =~ $live_hook_write_re ]] + local claude_home="${CLAUDE_HOME:-}" + local dollar='$' + local write_re='(tee|cat|cp|mv|rsync|install|chmod|chown|rm|trash)' + local live_hook_write_re="(tee|cat|cp|mv|rsync|install|chmod|chown|rm|trash)[^;&|]*((\\${dollar}CLAUDE_HOME|\\${dollar}\\{CLAUDE_HOME\\})/hooks|(\\${dollar}HOME|~|/Users/[^[:space:]/]+)?/\\.claude/hooks)" + [[ "$cmd" =~ $live_hook_write_re ]] && return 0 + [[ -n "$claude_home" && "$cmd" =~ $write_re && "$cmd" == *"$claude_home/hooks"* ]] +} + +path_is_live_claude_hooks() { + local path="$1" claude_home="${CLAUDE_HOME:-}" + case "$path" in + "$HOME/.claude/hooks"|"$HOME/.claude/hooks"/*) return 0 ;; + esac + if [[ -n "$claude_home" ]]; then + case "$path" in + "$claude_home/hooks"|"$claude_home/hooks"/*) return 0 ;; + esac + fi + return 1 } command_is_dangerous_outside_cwd() { @@ -655,23 +672,23 @@ handle_bash() { fi if cc_email_triage_active && command_is_raw_email_triage_gmail_mutation "$cmd"; then - deny "Raw Gmail mutation is blocked during email-triage. Phase 1 must use the VIVAZ runtime: vivaz-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then vivaz-email triage verify --latest --account . Only after verified Inbox Zero, open the queue." + deny "Raw Gmail mutation is blocked during email-triage. Phase 1 must use etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Only after verified Inbox Zero, open the queue." fi if cc_email_triage_active && command_is_email_triage_dry_run "$cmd" && ! command_is_email_triage_debug_dry_run "$cmd"; then - deny "Dry email-triage runs are blocked during /email-triage. Phase 1 must clear INBOX with vivaz-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then vivaz-email triage verify --latest --account before any queue item is shown." + deny "Dry email-triage runs are blocked during /email-triage. Phase 1 must clear INBOX with etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account before any queue item is shown." fi if cc_email_triage_active && command_is_email_triage_queue "$cmd" && ! cc_email_triage_verify_seen; then - deny "email-triage queue is blocked until Inbox Zero verification has run. First run vivaz-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then vivaz-email triage verify --latest --account . Open the queue only after verify reports inbox_zero_verified true and inbox_count 0." + deny "email-triage queue is blocked until Inbox Zero verification has run. First run etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Open the queue only after verify reports inbox_zero_verified true and inbox_count 0." fi if cc_email_triage_active && command_is_email_triage_queue "$cmd" && ! cc_email_triage_queue_verified "$cmd"; then - deny "email-triage queue is blocked until provider verification proves Inbox Zero and either gmail_mutated true or queue_ready_without_mutation true. Run vivaz-email triage verify --latest --account and require inbox_zero_verified true and inbox_count 0 before opening the queue." + deny "email-triage queue is blocked until provider verification proves Inbox Zero and either gmail_mutated true or queue_ready_without_mutation true. Run etrnl-email triage verify --latest --account and require inbox_zero_verified true and inbox_count 0 before opening the queue." fi if command_writes_live_claude_hooks "$cmd"; then - deny "Live ~/.claude/hooks edits are blocked. Edit the source-controlled Eternal Stack hook, run the installer, and verify source/install sync instead." + deny "Live Claude hook edits are blocked. Edit the source-controlled Eternal Stack hook, run the installer, and verify source/install sync instead." fi if command_is_gws_write "$cmd" && ! jq -e '.verificationRuns[]? | .value | test("gws.*(account|whoami)|gmail.*(account|whoami)|drive.*(account|whoami)")' "$(cc_state_file)" >/dev/null 2>&1; then @@ -726,11 +743,9 @@ handle_edit() { if violation="$(cc_policy_violation "$text")"; then deny "$violation" fi - case "$abs" in - "$HOME/.claude/hooks"|"$HOME/.claude/hooks"/*) - deny "Live ~/.claude/hooks edits are blocked. Edit source-controlled hooks and run the install/sync path." - ;; - esac + if path_is_live_claude_hooks "$abs"; then + deny "Live Claude hook edits are blocked: $abs. Edit source-controlled hooks and run the install/sync path." + fi if [[ -n "$abs" && "$current_tool" == "Write" && -e "$abs" && ! -f "$abs" && "$old_text_status" -ne 0 ]]; then deny "Cannot read existing content for safety checks: $abs" fi diff --git a/hooks/cc-stop-verifier.sh b/hooks/cc-stop-verifier.sh index 604d51a..701006b 100755 --- a/hooks/cc-stop-verifier.sh +++ b/hooks/cc-stop-verifier.sh @@ -30,8 +30,14 @@ source "$SCRIPT_DIR/lib/cleanup.sh" # [review/risk checks] -> allow cc_json_read_stdin -cc_json_require_jq || exit 0 -cc_json_valid || exit 0 +if ! cc_json_require_jq; then + printf '{"decision":"block","reason":"Stop verifier cannot run because jq is unavailable. Install jq or disable the guard explicitly before claiming completion."}\n' + exit 0 +fi +if ! cc_json_valid; then + cc_json_block "Stop verifier received invalid JSON. Re-run the completion gate with a valid hook payload before claiming completion." + exit 0 +fi cc_state_init if jq -e '.stop_hook_active == true' <<<"$HOOK_INPUT" >/dev/null; then @@ -124,7 +130,7 @@ cc_email_triage_run_command_after() { [.successfulCommands[]? | select((.at // "") >= $since) | (.command // "") - | select(test("(^|[[:space:];&|])([^[:space:];&|]*/)?vivaz-email[[:space:]]+triage[[:space:]]+(run|guarded-run)([[:space:]]|$)"))] + | select(test("(^|[[:space:];&|])([^[:space:];&|]*/)?etrnl-email[[:space:]]+triage[[:space:]]+(run|guarded-run)([[:space:]]|$)"))] | length > 0 ' <<<"$state" >/dev/null } @@ -135,7 +141,7 @@ cc_email_triage_output_run_id_after() { [.successfulCommands[]? | select((.at // "") >= $since) | (.command // "") - | select(test("(^|[[:space:];&|])([^[:space:];&|]*/)?vivaz-email[[:space:]]+triage[[:space:]]+(queue|report)([[:space:]]|$)")) + | select(test("(^|[[:space:];&|])([^[:space:];&|]*/)?etrnl-email[[:space:]]+triage[[:space:]]+(queue|report)([[:space:]]|$)")) | select(test("(^|[[:space:]])--run-id(=|[[:space:]]+)"))] | last // "" ' <<<"$state")" @@ -161,7 +167,7 @@ cc_email_triage_latest_account_after() { [.successfulCommands[]? | select((.at // "") >= $since) | (.command // "") - | select(test("(^|[[:space:];&|])([^[:space:];&|]*/)?vivaz-email[[:space:]]+triage[[:space:]]+(run|guarded-run)([[:space:]]|$)"))] + | select(test("(^|[[:space:];&|])([^[:space:];&|]*/)?etrnl-email[[:space:]]+triage[[:space:]]+(run|guarded-run)([[:space:]]|$)"))] | last // "" ' <<<"$state")" account="" @@ -173,7 +179,7 @@ cc_email_triage_latest_account_after() { cc_email_triage_cli() { local candidate resolved - for candidate in "${VIVAZ_EMAIL_BIN:-}" "${VIVAZ_EMAIL_CLI:-}"; do + for candidate in "${ETRNL_EMAIL_BIN:-}" "${ETRNL_EMAIL_CLI:-}"; do [[ -n "$candidate" ]] || continue if [[ -x "$candidate" ]]; then printf '%s\n' "$candidate" @@ -184,8 +190,8 @@ cc_email_triage_cli() { return 0 fi done - if command -v vivaz-email >/dev/null 2>&1; then - command -v vivaz-email + if command -v etrnl-email >/dev/null 2>&1; then + command -v etrnl-email return 0 fi return 1 @@ -370,15 +376,15 @@ if [[ "$claims_done" == "true" ]]; then if cc_email_triage_requested; then email_triage_since="$(cc_email_triage_request_at)" if ! cc_email_triage_evidence_after "$email_triage_since"; then - cc_json_block "email-triage phase 1 must clear INBOX first. Run vivaz-email triage guarded-run --account --max-inbox 500 --apply --require-insights and verify Inbox Zero before opening the action queue." + cc_json_block "email-triage phase 1 must clear INBOX first. Run etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights and verify Inbox Zero before opening the action queue." exit 0 fi if ! cc_email_triage_verify_latest "$email_triage_since"; then - cc_json_block "email-triage completion requires the latest vivaz-email triage ledger to verify successfully." + cc_json_block "email-triage completion requires the latest etrnl-email triage ledger to verify successfully." exit 0 fi if ! cc_email_triage_verify_applied "$email_triage_since"; then - cc_json_block "email-triage Inbox Zero completion requires provider-verified INBOX zero and either gmail_mutated true or queue_ready_without_mutation true. Use vivaz-email triage guarded-run --account --max-inbox 500 --apply --require-insights, verify inbox_count is 0, then open the action queue." + cc_json_block "email-triage Inbox Zero completion requires provider-verified INBOX zero and either gmail_mutated true or queue_ready_without_mutation true. Use etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, verify inbox_count is 0, then open the action queue." exit 0 fi if ! cc_email_triage_message_has_runtime_output; then diff --git a/hooks/cc-userprompt-router.sh b/hooks/cc-userprompt-router.sh index 1a36485..800e716 100755 --- a/hooks/cc-userprompt-router.sh +++ b/hooks/cc-userprompt-router.sh @@ -332,9 +332,9 @@ if [[ "$prompt_lower" =~ /email-triage|email[[:space:]-]+triage ]]; then email_triage_account="${BASH_REMATCH[2]}" fi if [[ -n "$email_triage_account" ]]; then - notes+=("Use /email-triage as two phases. Phase 1: Inbox Zero first. Run: vivaz-email triage guarded-run --account $email_triage_account --max-inbox 500 --apply --require-insights, then verify with vivaz-email triage verify --latest --account $email_triage_account. Do not open the queue unless verify reports inbox_zero_verified true and inbox_count 0. Phase 2: only after Inbox Zero, paste one generated queue item with vivaz-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") + notes+=("Use /email-triage as two phases. Phase 1: Inbox Zero first. Run: etrnl-email triage guarded-run --account $email_triage_account --max-inbox 500 --apply --require-insights, then verify with etrnl-email triage verify --latest --account $email_triage_account. Do not open the queue unless verify reports inbox_zero_verified true and inbox_count 0. Phase 2: only after Inbox Zero, paste one generated queue item with etrnl-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") else - notes+=("Use /email-triage as two phases. If no account id is present, ask for it. Phase 1: Inbox Zero first with vivaz-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then vivaz-email triage verify --latest --account . Do not open the queue unless verify reports inbox_zero_verified true and inbox_count 0. Phase 2: only after Inbox Zero, paste one generated queue item with vivaz-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") + notes+=("Use /email-triage as two phases. If no account id is present, ask for it. Phase 1: Inbox Zero first with etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Do not open the queue unless verify reports inbox_zero_verified true and inbox_count 0. Phase 2: only after Inbox Zero, paste one generated queue item with etrnl-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") fi fi if [[ "$prompt_lower" =~ disk[[:space:]-]+cleanup|clean[[:space:]]+up[[:space:]]+disk|free[[:space:]]+(disk|ssd|storage)[[:space:]]+space|reclaim[[:space:]]+(disk|ssd|storage)[[:space:]]+space ]]; then @@ -343,7 +343,7 @@ if [[ "$prompt_lower" =~ disk[[:space:]-]+cleanup|clean[[:space:]]+up[[:space:]] fi if [[ "$prompt_lower" =~ email[[:space:]-]+reply[[:space:]-]+quality|brazilian[[:space:]]+portuguese[[:space:]]+email|bad[[:space:]]+portuguese[[:space:]]+.*repl|em[[:space:]-]+dash.*email|humanize[[:space:]]+email[[:space:]]+reply|draft[[:space:]-]+checker|ai[[:space:]-]+tell.*email|vivaz[[:space:]]+email[[:space:]]+reply ]]; then record_skill "etrnl-comm-email-reply-quality" - notes+=("Use etrnl-comm-email-reply-quality: run vivaz-email drafts check, rewrite failed drafts with natural Brazilian Portuguese and humanizer cleanup, then rerun the checker before approval.") + notes+=("Use etrnl-comm-email-reply-quality: run etrnl-email drafts check, rewrite failed drafts with natural Brazilian Portuguese and humanizer cleanup, then rerun the checker before approval.") fi if [[ "$prompt_lower" =~ agent[[:space:]-]?files|instruction[[:space:]]+files|startup[[:space:]]+guidance|align[[:space:]]+.*agents\.md|align[[:space:]]+.*claude\.md|prune[[:space:]]+(agents|claude|rules)|rule[[:space:]-]?bloat|agents\.md[[:space:]]+(too[[:space:]]+long|too[[:space:]]+big|bloated)|claude\.md[[:space:]]+(too[[:space:]]+long|too[[:space:]]+big|bloated)|startup[[:space:]]+(file|context)[[:space:]]+(too[[:space:]]+long|bloated)|trim[[:space:]]+(agents|claude)\.md ]]; then record_skill "etrnl-ops-agent-files" diff --git a/rules-manifest.json b/rules-manifest.json index b454740..abe4461 100644 --- a/rules-manifest.json +++ b/rules-manifest.json @@ -4,14 +4,22 @@ "description": "Eternal Stack rules manifest. Tracks module metadata, host targets, checksums, and privacy gates for the eternal-saas rule pack.", "privacy": { "bannedTokens": [ - "metacards", - "mimo-finance", - "vivaz-website", - "sbcc-portal", - "rifasgg", - "finans" + "ETRNL_PRIVACY_SENTINEL_CLIENT", + "ETRNL_PRIVACY_SENTINEL_ACCOUNT", + "ETRNL_PRIVACY_SENTINEL_PROJECT", + "ETRNL_PRIVACY_SENTINEL_REPO", + "ETRNL_PRIVACY_SENTINEL_DOMAIN", + "ETRNL_PRIVACY_SENTINEL_EMAIL_RUNTIME", + "ETRNL_PRIVACY_SENTINEL_ACCOUNT_EMAIL", + "ETRNL_PRIVACY_SENTINEL_HOSTNAME", + "ETRNL_PRIVACY_SENTINEL_WORKSPACE", + "ETRNL_PRIVACY_SENTINEL_TRANSCRIPT" ], - "policy": "sync-rule-exports.mjs --check fails when any tracked rule file contains a banned token. Client repo names, account facts, credentials, and personal identity must stay in local gitignored overlays." + "localTokenFiles": [ + ".eternal/privacy-banned-tokens.local", + "rules-manifest.local.json" + ], + "policy": "sync-rule-exports.mjs --check fails when any tracked rule file contains a banned token. Tracked tokens are generic placeholders only. Client repo names, account facts, credentials, and personal identity must stay in local gitignored overlays." }, "profiles": { "eternal-saas": { @@ -51,14 +59,483 @@ "id": "Unique module identifier matching the frontmatter id field.", "paths": "Array of glob patterns for Claude Code paths: frontmatter scoping.", "globs": "Array of glob patterns for Cursor .mdc globs frontmatter.", - "hosts": "Array of target hosts: claude, codex, cursor.", + "hosts": "Array of target hosts for project rule modules: claude, cursor.", "maxBytes": "Optional per-module byte cap; doctor warns when exceeded.", "profile": "Profile or array of profiles this module belongs to.", "verify": "Shell command to verify the rule is effective in the target repo.", - "codexNested": "Relative directory path; when set, sync emits a nested AGENTS.md for Codex depth.", "checksum": "SHA-256 of the canonical module source file (populated by sync).", "generatedAt": "ISO timestamp of last sync run." } }, - "modules": {} + "modules": { + "global/00-stack": { + "id": "eternal-saas-global-stack", + "paths": [ + "**" + ], + "globs": [ + "**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "", + "checksum": "f3ca8cfe627608ebe93df51d1f6947706fc0fe4eaa2a0232bc61ccb4ca629e56", + "mdcChecksum": "e747324b82fa041cd51ee9afb986acee575bfa9da4f5a78c05cd6a840eaa0811", + "cursorPath": "templates/cursor/rules/eternal-saas/global/eternal-saas-global-stack.mdc", + "generatedAt": "2026-06-16T18:32:06.269Z" + }, + "global/10-gotchas": { + "id": "eternal-saas-global-gotchas", + "paths": [ + "**" + ], + "globs": [ + "**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "", + "checksum": "698233d7109523107053deadd63c8261cf92a3c2b3c4c2b9b3ea18ad10760b0b", + "mdcChecksum": "9884697f49a432b94fb133702debfe5f123e2d65eb42f0a4db1a41cef38f9c10", + "cursorPath": "templates/cursor/rules/eternal-saas/global/eternal-saas-global-gotchas.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "global/20-verify": { + "id": "eternal-saas-global-verify", + "paths": [ + "**" + ], + "globs": [ + "**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "", + "checksum": "d38a42fef2669bc11bb33a6f24abfbdfe91b42c3b989fd5dcf83e10a51b97da2", + "mdcChecksum": "7c8366e105aa168e41fd6a1a1f4793d5bb10cc09f371f2fd7f68d85cec8236b2", + "cursorPath": "templates/cursor/rules/eternal-saas/global/eternal-saas-global-verify.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/auth-tenancy": { + "id": "eternal-saas-auth-tenancy", + "paths": [ + "packages/domain/**", + "packages/core-domain/**", + "apps/web/src/lib/auth/**" + ], + "globs": [ + "packages/domain/**", + "packages/core-domain/**", + "apps/web/src/lib/auth/**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm guard:all", + "checksum": "d0da841e618581920401966a7b028ba01fddf391a717d897149fb1dbddf0c3d2", + "mdcChecksum": "bc5cb1502f4cbd1e72abe7b246fc56cd8c2800bc79a04c795866934db98e0851", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-auth-tenancy.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/components": { + "id": "eternal-saas-components", + "paths": [ + "apps/web/src/components/**", + "apps/show-web/src/components/**", + "apps/web/src/**/*.tsx", + "apps/show-web/src/**/*.tsx" + ], + "globs": [ + "apps/web/src/components/**", + "apps/show-web/src/components/**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm check", + "checksum": "39d8859d4ebe9bfeb942709e419b6283aa5c3d0c4893d7b80e8dea0e8401fe46", + "mdcChecksum": "2bfae692153f61a0172b2142309bcd8d0e3bd27d4c827a0e085a0cb0629aa598", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-components.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/deploy-onveloz": { + "id": "eternal-saas-deploy-onveloz", + "paths": [ + "veloz.json", + "next.config*", + "apps/web/next.config*" + ], + "globs": [ + "veloz.json", + "next.config*" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm ci:check", + "checksum": "711ce3a8d9ce9f4b3c0d6342249d42648b2b69813f573a6eaa7d928958e23397", + "mdcChecksum": "c70a1d95288dfc1d8bff39fce4ab0b9070721279f76645201234aa16522f5a33", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-deploy-onveloz.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/i18n": { + "id": "eternal-saas-i18n", + "paths": [ + "apps/web/src/**/*.tsx", + "apps/web/src/**/*.ts", + "apps/show-web/src/**/*.tsx" + ], + "globs": [ + "apps/web/src/**/*.tsx", + "apps/show-web/src/**/*.tsx" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm guard:essential", + "checksum": "4859717adb4a917d7840eaf19c1da33a352fad4b1f4a032b1860decbf2c4dd7d", + "mdcChecksum": "c480a11f45958cd6aa634979608ca16e898e43f040144b4431b7c5d25030268a", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-i18n.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/imports": { + "id": "eternal-saas-imports", + "paths": [ + "apps/web/src/lib/**", + "apps/web/src/app/api/**" + ], + "globs": [ + "apps/web/src/lib/**", + "apps/web/src/app/api/**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm guard:essential", + "checksum": "b64559303d82a7cad860614a81a8643c09185fec09388016a6f2272b15281542", + "mdcChecksum": "0935621b73782035013f47fe85395c02071b952f61723250036aa77cdf43fb2b", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-imports.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/local-overrides": { + "id": "eternal-saas-local-overrides", + "paths": [ + "**" + ], + "globs": [ + "**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "", + "checksum": "37f6668ba6cd3fce208a244c0f37c4281e5df92e7650207ed791e2916d8826a0", + "mdcChecksum": "ba414a5461210f99bdde895e34ac15e7cc55c37b65385f906d11d4361f32f4f6", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-local-overrides.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/money": { + "id": "eternal-saas-money", + "paths": [ + "packages/domain/**", + "packages/core-domain/**", + "apps/web/src/modules/billing/**" + ], + "globs": [ + "packages/domain/**", + "packages/core-domain/**", + "apps/web/src/modules/billing/**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm guard:essential", + "checksum": "f51e92c6588f95e3be27053f115b73c97c4ebc342f4981782877f36479f9f71b", + "mdcChecksum": "ca003e26ebefa8a5368bd7787a787830d28f66962195d1836cee89afc011319b", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-money.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/next": { + "id": "eternal-saas-next", + "paths": [ + "apps/web/src/app/**", + "apps/web/next.config*", + "apps/show-web/src/app/**" + ], + "globs": [ + "apps/web/src/app/**", + "apps/web/next.config*" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm check-types", + "checksum": "4280a03df5bd5499a560fb3095c7f42e0af4a83d727f751d5eb59aeb576d8b70", + "mdcChecksum": "d6ee69751e799f9f28d8e90b9f3a711cc1c941fe2d62e76c79e3af5a2392a929", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-next.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/orpc": { + "id": "eternal-saas-orpc", + "paths": [ + "apps/web/src/lib/orpc/**" + ], + "globs": [ + "apps/web/src/lib/orpc/**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm guard:essential", + "checksum": "d1deb6dad897247fde02f094b4a439d8e0ad653f527eda0f522bb7c6bbfb9558", + "mdcChecksum": "66c28e2301cc4b5c2471735ef1a0eb7cb7ba8f19142c242b069b3492dca46d4c", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-orpc.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/oxlint": { + "id": "eternal-saas-oxlint", + "paths": [ + "**/*.ts", + "**/*.tsx" + ], + "globs": [ + "**/*.ts", + "**/*.tsx" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm check", + "checksum": "b01fef5ff55e3dfda8c041ae332032bd85d5e13e3a1aafddb471cca08be45643", + "mdcChecksum": "10fbf5b5bc44e91f52db40081ad8dce76ba882258be46f1e453e8a8955416e46", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-oxlint.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/prisma": { + "id": "eternal-saas-prisma", + "paths": [ + "packages/db/**", + "packages/core-domain/**", + "apps/web/src/modules/**" + ], + "globs": [ + "packages/db/**", + "packages/core-domain/**", + "apps/web/src/modules/**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm guard:all", + "checksum": "ad9100b326b6fd745498c6a9c07f194d82cca6236348f7b5e6a42015691959e8", + "mdcChecksum": "6df83a374423fceec31cfe1b08a865f9beb569d6b8e962af26cfc097b90cd7db", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-prisma.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/react": { + "id": "eternal-saas-react", + "paths": [ + "apps/web/src/**/*.tsx", + "apps/show-web/src/**/*.tsx" + ], + "globs": [ + "apps/web/src/**/*.tsx", + "apps/show-web/src/**/*.tsx" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm check-types", + "checksum": "4efa234aa7843afd6e5c476a33a300b8685db5324c9959f5dc5f0ff63b24d9f6", + "mdcChecksum": "30325e003d5cb36eb46fad429de7a538f2c2b561c2da28836c846aea0c2c4563", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-react.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/stack": { + "id": "eternal-saas-stack", + "paths": [ + "**" + ], + "globs": [ + "**" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm ci:check", + "checksum": "f9d711659ffd7d5d6523dcf3049561ef1785da71510392e66ec96f8351164470", + "mdcChecksum": "ec4178d27ecec5afdea7cd6af2aa8e8ef61f671a53a63d9a6aef1c15bb23bd54", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-stack.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/tcg-contract": { + "id": "eternal-saas-tcg-contract", + "paths": [ + "apps/**/*tcg*/**", + "apps/**/*tcg-card*/**", + "apps/**/*trading-card*/**", + "apps/**/*collectible-card*/**", + "packages/**/*tcg*/**", + "packages/**/*tcg-card*/**", + "packages/**/*trading-card*/**", + "packages/**/*collectible-card*/**" + ], + "globs": [ + "apps/**/tcg*", + "apps/**/tcg-card*", + "apps/**/trading-card*", + "apps/**/collectible-card*", + "packages/**/tcg*", + "packages/**/tcg-card*", + "packages/**/trading-card*", + "packages/**/collectible-card*" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas-tcg" + ], + "verify": "pnpm test", + "checksum": "64aa62c9bec37d35493c51b4569c387ec1cbcbff5d945fa4ce7224166a8e2cf7", + "mdcChecksum": "077c3e9b44f482cfc41d4e92852e3b12981c1a89cf54f71e75a071bdae5ffba2", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-tcg-contract.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/testing": { + "id": "eternal-saas-testing", + "paths": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.property.test.ts" + ], + "globs": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm test", + "checksum": "1a5aeb8a6f321d3d32e47e2861924a0d8e9f5ba948a513873f1a157d0e69565c", + "mdcChecksum": "bb308c25bebe0522aa8f938388050bcdcc50cff571d78083c76c415500c69978", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-testing.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + }, + "project/typescript": { + "id": "eternal-saas-typescript", + "paths": [ + "**/*.ts", + "**/*.tsx" + ], + "globs": [ + "**/*.ts", + "**/*.tsx" + ], + "hosts": [ + "claude", + "cursor" + ], + "profile": [ + "eternal-saas", + "eternal-saas-tcg" + ], + "verify": "pnpm check-types", + "checksum": "d6895dc3083cef16598495c4c516cdd3c880f704f83c85f4f2be897f50dccfff", + "mdcChecksum": "fc7c872184ffbc65ce16f410bfdc1707dd94a9d3663a6760c24188e834571a35", + "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-typescript.mdc", + "generatedAt": "2026-06-16T18:32:06.270Z" + } + } } diff --git a/rules/eternal-saas/global/00-stack.md b/rules/eternal-saas/global/00-stack.md index f921059..5f817d5 100644 --- a/rules/eternal-saas/global/00-stack.md +++ b/rules/eternal-saas/global/00-stack.md @@ -2,7 +2,7 @@ id: eternal-saas-global-stack globs: ["**"] description: "Eternal SaaS stack overview: tech choices, commands, deployment surface." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] alwaysApply: false --- diff --git a/rules/eternal-saas/global/10-gotchas.md b/rules/eternal-saas/global/10-gotchas.md index d054220..7f8ef90 100644 --- a/rules/eternal-saas/global/10-gotchas.md +++ b/rules/eternal-saas/global/10-gotchas.md @@ -2,7 +2,7 @@ id: eternal-saas-global-gotchas globs: ["**"] description: "Top 15 cross-repo mistakes in the Eternal SaaS stack." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] alwaysApply: false --- diff --git a/rules/eternal-saas/global/20-verify.md b/rules/eternal-saas/global/20-verify.md index 40c7913..2f0fdc7 100644 --- a/rules/eternal-saas/global/20-verify.md +++ b/rules/eternal-saas/global/20-verify.md @@ -2,7 +2,7 @@ id: eternal-saas-global-verify globs: ["**"] description: "Verify commands reference for the Eternal SaaS stack." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] alwaysApply: false --- diff --git a/rules/eternal-saas/project/auth-tenancy.md b/rules/eternal-saas/project/auth-tenancy.md index a2f129e..fa69cb8 100644 --- a/rules/eternal-saas/project/auth-tenancy.md +++ b/rules/eternal-saas/project/auth-tenancy.md @@ -9,7 +9,7 @@ globs: - "packages/core-domain/**" - "apps/web/src/lib/auth/**" description: "Auth and tenant isolation: context helpers, Better-Auth, tenant-safe queries." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm guard:all" --- diff --git a/rules/eternal-saas/project/components.md b/rules/eternal-saas/project/components.md index 85a2f0e..aabfab6 100644 --- a/rules/eternal-saas/project/components.md +++ b/rules/eternal-saas/project/components.md @@ -9,7 +9,7 @@ globs: - "apps/web/src/components/**" - "apps/show-web/src/components/**" description: "Component patterns: design system imports, no barrel exports, design system usage." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm check" --- @@ -19,11 +19,11 @@ verify: "pnpm check" ```typescript // CORRECT -import { Button } from '@core-suite/design-system/primitives' -import { Input } from '@core-suite/design-system/primitives' +import { Button } from '@example-suite/design-system/primitives' +import { Input } from '@example-suite/design-system/primitives' // WRONG — barrel import -import { Button, Input } from '@core-suite/design-system' +import { Button, Input } from '@example-suite/design-system' ``` ## React Compiler active — no manual memoization diff --git a/rules/eternal-saas/project/deploy-onveloz.md b/rules/eternal-saas/project/deploy-onveloz.md index eec4a55..55aacf6 100644 --- a/rules/eternal-saas/project/deploy-onveloz.md +++ b/rules/eternal-saas/project/deploy-onveloz.md @@ -8,7 +8,7 @@ globs: - "veloz.json" - "next.config*" description: "Onveloz deployment rules: manual deploy, no standalone output, env var safety." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm ci:check" --- diff --git a/rules/eternal-saas/project/i18n.md b/rules/eternal-saas/project/i18n.md index 39d83e9..640f521 100644 --- a/rules/eternal-saas/project/i18n.md +++ b/rules/eternal-saas/project/i18n.md @@ -8,7 +8,7 @@ globs: - "apps/web/src/**/*.tsx" - "apps/show-web/src/**/*.tsx" description: "i18n rules: next-intl v4, required locales, no hardcoded strings, type-cast patterns." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm guard:essential" --- diff --git a/rules/eternal-saas/project/imports.md b/rules/eternal-saas/project/imports.md index c546b20..eac181f 100644 --- a/rules/eternal-saas/project/imports.md +++ b/rules/eternal-saas/project/imports.md @@ -7,7 +7,7 @@ globs: - "apps/web/src/lib/**" - "apps/web/src/app/api/**" description: "Import rules for lib/ and app/api/: modules-only, no features imports." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm guard:essential" --- diff --git a/rules/eternal-saas/project/local-overrides.md b/rules/eternal-saas/project/local-overrides.md index 8c5ea5d..f9c1755 100644 --- a/rules/eternal-saas/project/local-overrides.md +++ b/rules/eternal-saas/project/local-overrides.md @@ -5,7 +5,7 @@ paths: globs: - "**" description: "Project-specific overrides for eternal-saas rules. Edit this file after init." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] alwaysApply: false --- diff --git a/rules/eternal-saas/project/money.md b/rules/eternal-saas/project/money.md index ab35d05..5f6d221 100644 --- a/rules/eternal-saas/project/money.md +++ b/rules/eternal-saas/project/money.md @@ -9,7 +9,7 @@ globs: - "packages/core-domain/**" - "apps/web/src/modules/billing/**" description: "Money handling: Money value object, Prisma.Decimal conversion, DEFAULT_CURRENCY." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm guard:essential" --- diff --git a/rules/eternal-saas/project/next.md b/rules/eternal-saas/project/next.md index efc3219..97cac4d 100644 --- a/rules/eternal-saas/project/next.md +++ b/rules/eternal-saas/project/next.md @@ -8,7 +8,7 @@ globs: - "apps/web/src/app/**" - "apps/web/next.config*" description: "Next.js rules: output mode, env vars, proxy config, server vs client logger." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm check-types" --- diff --git a/rules/eternal-saas/project/orpc.md b/rules/eternal-saas/project/orpc.md index d186563..04163c0 100644 --- a/rules/eternal-saas/project/orpc.md +++ b/rules/eternal-saas/project/orpc.md @@ -5,7 +5,7 @@ paths: globs: - "apps/web/src/lib/orpc/**" description: "oRPC route patterns: middleware order, auth context, error handling, deprecated packages." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm guard:essential" --- diff --git a/rules/eternal-saas/project/oxlint.md b/rules/eternal-saas/project/oxlint.md index 3365789..7fc3702 100644 --- a/rules/eternal-saas/project/oxlint.md +++ b/rules/eternal-saas/project/oxlint.md @@ -7,7 +7,7 @@ globs: - "**/*.ts" - "**/*.tsx" description: "Linting and formatting rules: oxlint, oxfmt, no suppressions, Semgrep double-comment." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm check" --- diff --git a/rules/eternal-saas/project/prisma.md b/rules/eternal-saas/project/prisma.md index 030888a..bc299ce 100644 --- a/rules/eternal-saas/project/prisma.md +++ b/rules/eternal-saas/project/prisma.md @@ -9,7 +9,7 @@ globs: - "packages/core-domain/**" - "apps/web/src/modules/**" description: "Prisma/DB rules: tenant isolation, PgBouncer crash, transactions, soft deletes, schema changes." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm guard:all" --- diff --git a/rules/eternal-saas/project/react.md b/rules/eternal-saas/project/react.md index f297c60..26ad35e 100644 --- a/rules/eternal-saas/project/react.md +++ b/rules/eternal-saas/project/react.md @@ -7,7 +7,7 @@ globs: - "apps/web/src/**/*.tsx" - "apps/show-web/src/**/*.tsx" description: "React patterns: React Compiler rules, memoization ban, client vs server logger." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm check-types" --- diff --git a/rules/eternal-saas/project/stack.md b/rules/eternal-saas/project/stack.md index f55a9a4..99c9118 100644 --- a/rules/eternal-saas/project/stack.md +++ b/rules/eternal-saas/project/stack.md @@ -5,7 +5,7 @@ paths: globs: - "**" description: "Project stack overview for Eternal SaaS repos: tech, architecture, commands reference." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] alwaysApply: false verify: "pnpm ci:check" --- diff --git a/rules/eternal-saas/project/tcg-contract.md b/rules/eternal-saas/project/tcg-contract.md new file mode 100644 index 0000000..77f0c49 --- /dev/null +++ b/rules/eternal-saas/project/tcg-contract.md @@ -0,0 +1,41 @@ +--- +id: eternal-saas-tcg-contract +paths: + - "apps/**/*tcg*/**" + - "apps/**/*tcg-card*/**" + - "apps/**/*trading-card*/**" + - "apps/**/*collectible-card*/**" + - "packages/**/*tcg*/**" + - "packages/**/*tcg-card*/**" + - "packages/**/*trading-card*/**" + - "packages/**/*collectible-card*/**" +globs: + - "apps/**/tcg*" + - "apps/**/tcg-card*" + - "apps/**/trading-card*" + - "apps/**/collectible-card*" + - "packages/**/tcg*" + - "packages/**/tcg-card*" + - "packages/**/trading-card*" + - "packages/**/collectible-card*" +description: "TCG rules: collectible-card domain contracts, inventory safety, and market-data verification." +hosts: [claude, cursor] +alwaysApply: false +verify: "pnpm test" +--- + +# TCG Contract Rules + +Use these rules for collectible-card marketplace, inventory, pricing, and catalog workflows. + +## Domain contracts + +- Treat card identity, edition, language, condition, finish, and quantity as separate fields. +- Validate external marketplace and catalog data at the import boundary before it reaches domain logic. +- Keep inventory mutation flows auditable: every stock adjustment needs a source event or operator action. + +## Price and market data + +- Store the source, captured timestamp, currency, and condition assumptions with every market snapshot. +- Do not mix retail listing prices, completed-sale prices, and manually-entered fallback prices without a typed source discriminator. +- Reconcile duplicate marketplace matches before accepting automated bulk updates. diff --git a/rules/eternal-saas/project/testing.md b/rules/eternal-saas/project/testing.md index 6b2a623..5e82818 100644 --- a/rules/eternal-saas/project/testing.md +++ b/rules/eternal-saas/project/testing.md @@ -10,7 +10,7 @@ globs: - "**/*.spec.ts" - "**/*.test.tsx" description: "Testing rules: AAA pattern, determinism, mock factories, assertion quality." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm test" --- diff --git a/rules/eternal-saas/project/typescript.md b/rules/eternal-saas/project/typescript.md index 7c35b37..8c4055f 100644 --- a/rules/eternal-saas/project/typescript.md +++ b/rules/eternal-saas/project/typescript.md @@ -7,7 +7,7 @@ globs: - "**/*.ts" - "**/*.tsx" description: "TypeScript rules: no any, Zod nullability, typed env config, no suppressions." -hosts: [claude, codex, cursor] +hosts: [claude, cursor] verify: "pnpm check-types" --- diff --git a/rules/etrnl/domains.md b/rules/etrnl/domains.md index 6fedb2c..e0e90a4 100644 --- a/rules/etrnl/domains.md +++ b/rules/etrnl/domains.md @@ -2,5 +2,7 @@ - Invoke `eternal-best-practices` before editing domain-sensitive surfaces. - Domain-sensitive surfaces include auth, tenancy, billing, payments, money values, i18n, Prisma/schema, permissions, and soft deletes. -- Use bundled stack skills: `eternal-best-practices`, `code-simplifier`, `finding-duplicate-functions`, Brooks modules via `/etrnl-code-review-excellence`, and domain skills (`domain-*`, `better-auth`, `tenant-isolation-patterns`, `money-vo-discipline`, `prisma-expert`, `i18n-localization`, `stripe-best-practices`, `abacatepay-integration`). +- Use bundled stack skills: `eternal-best-practices`, `code-simplifier`, `finding-duplicate-functions`, and Brooks modules via `/etrnl-code-review-excellence`. +- For domain-sensitive work, use `domain-cli`, `domain-cloud-native`, `domain-embedded`, `domain-fintech`, `domain-iot`, `domain-ml`, and `domain-web`. +- For auth, tenancy, money, Prisma, i18n, and payments work, use `better-auth`, `tenant-isolation-patterns`, `money-vo-discipline`, `prisma-expert`, `i18n-localization`, `stripe-best-practices`, and `abacatepay-integration`. - If a bundled skill is missing from the host, state that explicitly and run the closest manual review pass. diff --git a/scripts/bootstrap-tools.sh b/scripts/bootstrap-tools.sh index f38c5bc..7859e67 100755 --- a/scripts/bootstrap-tools.sh +++ b/scripts/bootstrap-tools.sh @@ -12,6 +12,8 @@ SKIP_CODEGRAPH=0 SKIP_BEADS=0 SKIP_HINDSIGHT=0 DRY_RUN=0 +CODEGRAPH_NPM_SPEC="${ETRNL_CODEGRAPH_NPM_SPEC:-@colbymchenry/codegraph@1.0.1}" +BEADS_NPM_SPEC="${ETRNL_BEADS_NPM_SPEC:-@beads/bd@1.0.5}" CONFIRM_SKIPPED=64 PROFILE="${ETRNL_STACK_PROFILE:-core}" HINDSIGHT_MODE="${ETRNL_HINDSIGHT_MODE:-local-daemon}" @@ -185,7 +187,7 @@ install_codegraph() { return 0 fi if [[ "$DRY_RUN" == "1" ]]; then - printf 'dry-run: would install/verify CodeGraph package @colbymchenry/codegraph\n' + printf 'dry-run: would install/verify CodeGraph package %s\n' "$CODEGRAPH_NPM_SPEC" printf 'dry-run: would refresh CodeGraph MCP config for supported agents\n' return 0 fi @@ -198,9 +200,9 @@ install_codegraph() { [[ "$confirm_status" == "$CONFIRM_SKIPPED" ]] && return 0 [[ "$confirm_status" == "0" ]] || return "$confirm_status" npm_status=0 - npm install -g @colbymchenry/codegraph || npm_status=$? + npm install -g "$CODEGRAPH_NPM_SPEC" || npm_status=$? if [[ "$npm_status" != "0" ]]; then - printf 'bootstrap error: npm install failed for @colbymchenry/codegraph (exit %s)\n' "$npm_status" >&2 + printf 'bootstrap error: npm install failed for %s (exit %s)\n' "$CODEGRAPH_NPM_SPEC" "$npm_status" >&2 return "$npm_status" fi need_command codegraph || { printf 'bootstrap error: codegraph binary not found after npm install\n' >&2; return 1; } @@ -221,7 +223,7 @@ install_beads() { return 0 fi if [[ "$DRY_RUN" == "1" ]]; then - printf 'dry-run: would install/verify Beads package @beads/bd without raw setup hooks\n' + printf 'dry-run: would install/verify Beads package %s without raw setup hooks\n' "$BEADS_NPM_SPEC" return 0 fi if need_command bd; then @@ -234,9 +236,9 @@ install_beads() { [[ "$confirm_status" == "$CONFIRM_SKIPPED" ]] && return 0 [[ "$confirm_status" == "0" ]] || return "$confirm_status" npm_status=0 - npm install -g @beads/bd || npm_status=$? + npm install -g "$BEADS_NPM_SPEC" || npm_status=$? if [[ "$npm_status" != "0" ]]; then - printf 'bootstrap error: npm install failed for @beads/bd (exit %s)\n' "$npm_status" >&2 + printf 'bootstrap error: npm install failed for %s (exit %s)\n' "$BEADS_NPM_SPEC" "$npm_status" >&2 return "$npm_status" fi need_command bd || { printf 'bootstrap error: bd binary not found after npm install\n' >&2; return 1; } diff --git a/scripts/changelog-release-check.mjs b/scripts/changelog-release-check.mjs index 075ca00..6de39e8 100755 --- a/scripts/changelog-release-check.mjs +++ b/scripts/changelog-release-check.mjs @@ -23,9 +23,10 @@ const KEEP_A_CHANGELOG_CATEGORIES = new Set([ ]); function usage() { - console.error("usage: changelog-release-check.mjs [--root ] [--allow-unreleased] [--strict-unreleased] [--allow-clean-history-changelog] [--skip-version-file] [--skip-categories]"); + console.error("usage: changelog-release-check.mjs [--root ] [--allow-unreleased] [--strict-unreleased] [--allow-clean-history-changelog] [--allow-pending-release] [--skip-version-file] [--skip-categories]"); console.error("--strict-unreleased takes precedence over --allow-unreleased when both are present."); console.error("--allow-clean-history-changelog permits older changelog sections without tags after a clean-root public release."); + console.error("--allow-pending-release permits VERSION to point at an untagged top changelog section for PR health checks."); console.error("--skip-version-file skips VERSION file alignment checks (test fixtures only)."); console.error("--skip-categories skips Keep a Changelog category validation (test fixtures only)."); process.exit(2); @@ -225,9 +226,9 @@ function validateVersionFile(root, topRelease, skipVersionFile) { return errors; } -function validateTopReleaseTagged(root, topRelease, skipVersionFile) { +function validateTopReleaseTagged(root, topRelease, skipVersionFile, allowPendingRelease) { const errors = []; - if (skipVersionFile || !topRelease) return errors; + if (skipVersionFile || allowPendingRelease || !topRelease) return errors; const versionPath = path.join(root, "VERSION"); if (!existsSync(versionPath)) return errors; const version = readFileSync(versionPath, "utf8").trim().replace(/^v/i, ""); @@ -289,6 +290,7 @@ function validateUntaggedReleaseDrift(root, releaseSections, allowCleanHistoryCh const root = path.resolve(argValue("--root", path.join(scriptDir, ".."))); const allowUnreleased = args.includes("--allow-unreleased") && !args.includes("--strict-unreleased"); const allowCleanHistoryChangelog = args.includes("--allow-clean-history-changelog"); +const allowPendingRelease = args.includes("--allow-pending-release"); const skipVersionFile = args.includes("--skip-version-file"); const skipCategories = args.includes("--skip-categories"); const changelogPath = path.join(root, "CHANGELOG.md"); @@ -312,7 +314,7 @@ if (unreleasedEntries.length > 0 && !allowUnreleased) { } errors.push(...validateReleaseCategories(lines, releaseSections, skipCategories)); errors.push(...validateVersionFile(root, topRelease, skipVersionFile)); -errors.push(...validateTopReleaseTagged(root, topRelease, skipVersionFile)); +errors.push(...validateTopReleaseTagged(root, topRelease, skipVersionFile, allowPendingRelease)); errors.push(...validateGitTagAlignment(root, releaseVersions, topRelease)); errors.push(...validateUntaggedReleaseDrift(root, releaseSections, allowCleanHistoryChangelog)); diff --git a/scripts/code-health-ledger-check.mjs b/scripts/code-health-ledger-check.mjs index f963be5..43510af 100644 --- a/scripts/code-health-ledger-check.mjs +++ b/scripts/code-health-ledger-check.mjs @@ -64,6 +64,13 @@ function latestCodeHealthRequest(state) { .filter((item) => norm(item?.value) === CODE_HEALTH_SKILL) .map((item) => parseStamp(stamp(item))) .filter(Number.isFinite); + const prompt = String(state.lastPrompt || "").toLowerCase(); + if (/code[- ]health|repo[- ]health|codebase[- ]health|no skips|whole codebase audit|entire codebase audit/.test(prompt)) { + const startedAt = parseStamp(state.startedAt); + if (Number.isFinite(startedAt)) { + times.push(startedAt); + } + } return times.length > 0 ? Math.max(...times) : 0; } diff --git a/scripts/deep-audit-artifact-check.mjs b/scripts/deep-audit-artifact-check.mjs index 7a289a5..ef77760 100644 --- a/scripts/deep-audit-artifact-check.mjs +++ b/scripts/deep-audit-artifact-check.mjs @@ -175,7 +175,7 @@ function worklistHashes(artifact) { return Object.fromEntries(objectEntries(artifact.worklists).map(([id, worklist]) => [id, worklist?.sha256 || worklist?.hash || ""])); } -function validateConsumedHashes(item, category, artifact, artifactPath, errors, jsonPath) { +function validateConsumedHashes(item, category, artifact, artifactPath, errors, jsonPath, requiredWorklists = category.requiredWorklists) { const consumed = item?.consumedWorklistHashes; if (!consumed || typeof consumed !== "object" || Array.isArray(consumed)) { errors.push(diagnostic("CONSUMED_WORKLIST_HASHES_MISSING", artifactPath, `${jsonPath} lacks consumedWorklistHashes.`, "Category reports and lane receipts must prove they consumed shared worklists.", "Copy the shared worklist hashes into consumedWorklistHashes.", jsonPath)); @@ -183,7 +183,7 @@ function validateConsumedHashes(item, category, artifact, artifactPath, errors, } const hashes = worklistHashes(artifact); const worklists = artifact.worklists && typeof artifact.worklists === "object" ? artifact.worklists : {}; - for (const worklistId of category.requiredWorklists) { + for (const worklistId of requiredWorklists) { if (!hasOwn(worklists, worklistId)) { errors.push(diagnostic("REQUIRED_WORKLIST_MISSING", artifactPath, `${jsonPath} cannot find required shared worklist ${worklistId}.`, "Selected categories must consume every required worklist from the orchestrator inventory.", `Add ${worklistId} to worklists with sha256 and artifactLabel.`, `$.worklists.${worklistId}`)); continue; @@ -391,7 +391,7 @@ function validateLaneReceipts(artifact, artifactPath, errors, selected) { if (!receipt.summary) { errors.push(diagnostic("LANE_RECEIPT_SUMMARY_MISSING", artifactPath, `${category.categoryId}/${lane.laneId} has no summary.`, "Fanout receipts need a human-readable completion summary before synthesis.", "Add a non-empty summary.", `${receiptPath}.summary`)); } - validateConsumedHashes(receipt, category, artifact, artifactPath, errors, receiptPath); + validateConsumedHashes(receipt, category, artifact, artifactPath, errors, receiptPath, lane.allowedWorklists || category.requiredWorklists); } } } diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 9de4667..fe41b82 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -42,10 +42,7 @@ DOCTOR_HEAVY_STARTED=0 # shellcheck source=scripts/lib/skill-lists.sh source "$ROOT/scripts/lib/skill-lists.sh" -doctor_cleanup() { - rm -rf -- "$DOCTOR_RESULT_DIR" -} -trap doctor_cleanup EXIT +trap 'rm -rf -- "$DOCTOR_RESULT_DIR"' EXIT ok() { printf 'ok: %s\n' "$*"; } fail() { printf 'fail: %s\n' "$*" >&2; STATUS=1; } @@ -325,7 +322,7 @@ check_startup_file_budget() { fi } -for dep in jq git node rg fd; do +for dep in jq git node rg fd shellcheck; do require_command "$dep" done if [[ -f "$ROOT/scripts/bootstrap-tools.sh" ]]; then @@ -404,6 +401,9 @@ else fail "codex RTK hook script missing" fi run_parallel_syntax_checks +if [[ -d "$ROOT/hooks" && -d "$ROOT/scripts" && -d "$ROOT/tests" ]]; then + report_command "shellcheck clean" "shellcheck failed" fd -t f -e sh . "$ROOT/hooks" "$ROOT/scripts" "$ROOT/tests" -X shellcheck -x +fi if [[ -f "$ROOT/scripts/lib/read-stdin.mjs" ]]; then report_command "read-stdin helper syntax valid" "read-stdin helper syntax invalid" node --check "$ROOT/scripts/lib/read-stdin.mjs" else @@ -636,14 +636,14 @@ codex_warn_threshold=$(( codex_byte_budget * 75 / 100 )) if [[ -f "$codex_target/AGENTS.md" ]]; then agents_bytes="$(wc -c < "$codex_target/AGENTS.md" | tr -d ' ')" if (( agents_bytes > codex_byte_budget )); then - fail "~/.codex/AGENTS.md exceeds byte budget ($agents_bytes > $codex_byte_budget)" + fail "\$HOME/.codex/AGENTS.md exceeds byte budget ($agents_bytes > $codex_byte_budget)" elif (( agents_bytes > codex_warn_threshold )); then - fail "~/.codex/AGENTS.md at $agents_bytes bytes (>75% of $codex_byte_budget budget)" + fail "\$HOME/.codex/AGENTS.md at $agents_bytes bytes (>75% of $codex_byte_budget budget)" else - ok "~/.codex/AGENTS.md within byte budget ($agents_bytes / $codex_byte_budget)" + ok "\$HOME/.codex/AGENTS.md within byte budget ($agents_bytes / $codex_byte_budget)" fi else - ok "~/.codex/AGENTS.md not installed (ETRNL_INSTALL_STARTUP gated)" + ok "\$HOME/.codex/AGENTS.md not installed (ETRNL_INSTALL_STARTUP gated)" fi optional_command gemini "optional Gemini escalation available" "optional Gemini escalation not installed" optional_command playwright-cli "optional browser QA tool available" "optional browser QA tool not installed" @@ -681,6 +681,15 @@ if [[ -f "$ROOT/rules-manifest.json" ]]; then else fail "rules-manifest.json privacy.bannedTokens is empty — privacy gate inactive" fi + module_count="$(jq -r '.modules | length' "$ROOT/rules-manifest.json" 2>/dev/null || echo "0")" + if (( module_count > 0 )); then + ok "rules-manifest.json modules indexed=$module_count" + else + fail "rules-manifest.json modules index is empty" + fi + if [[ -f "$ROOT/scripts/sync-rule-exports.mjs" ]]; then + report_command "rule exports synchronized" "rule export sync drift" node "$ROOT/scripts/sync-rule-exports.mjs" --check + fi else fail "rules-manifest.json invalid JSON" fi @@ -772,7 +781,7 @@ if [[ -f "$claude_home/etrnl/install.json" ]]; then fi if [[ -f "$ROOT/scripts/changelog-release-check.mjs" && -f "$ROOT/CHANGELOG.md" ]]; then - if changelog_out="$(node "$ROOT/scripts/changelog-release-check.mjs" --strict-unreleased --allow-clean-history-changelog 2>&1)"; then + if changelog_out="$(node "$ROOT/scripts/changelog-release-check.mjs" --strict-unreleased --allow-clean-history-changelog --allow-pending-release 2>&1)"; then while IFS= read -r line; do [[ -n "$line" ]] && ok "changelog: $line" done <<<"$changelog_out" @@ -827,6 +836,64 @@ if git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1 && [[ -f "$ROO else ok "credential pattern scan clean" fi + if [[ -f "$ROOT/rules-manifest.json" ]]; then + if ! command -v python3 >/dev/null 2>&1; then + fail "python3 unavailable; privacy banned-token scan cannot run" + elif privacy_out="$(python3 - "$ROOT" <<'PYEOF' 2>&1 +import json +import subprocess +import sys +from pathlib import Path + +root = Path(sys.argv[1]) +manifest = json.loads((root / "rules-manifest.json").read_text()) +tokens = [str(token).lower() for token in manifest.get("privacy", {}).get("bannedTokens", []) if str(token).strip()] +local_token_files = {str(rel_path) for rel_path in manifest.get("privacy", {}).get("localTokenFiles", [])} +for rel_path in manifest.get("privacy", {}).get("localTokenFiles", []): + local_path = root / str(rel_path) + if not local_path.exists(): + continue + if local_path.suffix == ".json": + parsed = json.loads(local_path.read_text()) + if isinstance(parsed, list): + local_tokens = parsed + else: + local_tokens = parsed.get("privacy", {}).get("bannedTokens", parsed.get("bannedTokens", [])) + else: + local_tokens = [ + line.strip() + for line in local_path.read_text().splitlines() + if line.strip() and not line.strip().startswith("#") + ] + tokens.extend(str(token).lower() for token in local_tokens if str(token).strip()) +tokens = sorted(set(tokens)) +files = subprocess.check_output(["git", "-C", str(root), "ls-files"], text=True).splitlines() +violations = [] +for rel in files: + if rel == "rules-manifest.json" or rel in local_token_files: + continue + path = root / rel + try: + if path.stat().st_size > 10 * 1024 * 1024: + continue + text = path.read_text(errors="ignore").lower() + except OSError: + continue + found_count = sum(1 for token in tokens if token in text) + if found_count: + violations.append(f"{rel}: banned token match count={found_count}") +if violations: + print("\n".join(violations)) + sys.exit(1) +PYEOF + )"; then + ok "privacy banned-token scan clean" + else + while IFS= read -r line; do + [[ -n "$line" ]] && fail "privacy banned-token: $line" + done <<<"$privacy_out" + fi + fi else ok "credential scan skipped outside source checkout" fi diff --git a/scripts/init-project-rules.sh b/scripts/init-project-rules.sh index ce5aa53..d39b0a9 100755 --- a/scripts/init-project-rules.sh +++ b/scripts/init-project-rules.sh @@ -6,10 +6,9 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PACK_ROOT="$ROOT/rules/eternal-saas" +CURSOR_PACK_ROOT="$ROOT/templates/cursor/rules/eternal-saas" MANIFEST_SOURCE="$ROOT/rules-manifest.json" -# ── argument parsing ──────────────────────────────────────────────────────── - PROFILE="" DRY_RUN=0 CHECK_MODE=0 @@ -26,8 +25,8 @@ while [[ $# -gt 0 ]]; do PROFILE="$2"; shift 2 ;; --dry-run) DRY_RUN=1; shift ;; - --check) CHECK_MODE=1; shift ;; - --force) FORCE=1; shift ;; + --check) CHECK_MODE=1; shift ;; + --force) FORCE=1; shift ;; -*) echo "error: unknown flag: $1" >&2; exit 1 ;; *) TARGET="$1"; shift ;; esac @@ -45,88 +44,155 @@ fi TARGET="$(realpath "$TARGET")" -if [[ "$PROFILE" != "eternal-saas" && "$PROFILE" != "eternal-saas-tcg" ]]; then - echo "error: unknown profile '$PROFILE'. Valid: eternal-saas, eternal-saas-tcg" >&2 - exit 1 -fi - -# ── destination paths ─────────────────────────────────────────────────────── - CLAUDE_RULES_DEST="$TARGET/.claude/rules/eternal-saas" CURSOR_RULES_DEST="$TARGET/.cursor/rules/eternal-saas" MANIFEST_RECEIPT="$CLAUDE_RULES_DEST/.manifest.json" -# ── helper: compute sha256 of a file ─────────────────────────────────────── - file_sha256() { shasum -a 256 "$1" 2>/dev/null | awk '{print $1}' || sha256sum "$1" | awk '{print $1}' } -# ── collect source modules for profile ───────────────────────────────────── +receipt_checksum_for() { + local sums="$1" + local rel="$2" + awk -v key="$rel" '$1 == key {print $2; exit}' <<<"$sums" +} + +receipt_installed_at() { + python3 - "$MANIFEST_RECEIPT" <<'PYEOF' +import json +import sys +from pathlib import Path + +try: + receipt = json.loads(Path(sys.argv[1]).read_text()) +except Exception as error: + raise SystemExit(f"error: failed to parse manifest receipt {sys.argv[1]}: {error}") + +print(receipt.get("installedAt", "")) +PYEOF +} + +receipt_checksums() { + local field="$1" + python3 - "$MANIFEST_RECEIPT" "$field" <<'PYEOF' +import json +import sys +from pathlib import Path + +try: + receipt = json.loads(Path(sys.argv[1]).read_text()) +except Exception as error: + raise SystemExit(f"error: failed to parse manifest receipt {sys.argv[1]}: {error}") + +items = receipt.get(sys.argv[2], {}) +if not isinstance(items, dict): + raise SystemExit(f"error: manifest receipt field {sys.argv[2]} must be an object") + +for key, value in items.items(): + print(key, value) +PYEOF +} collect_modules() { - local profile="$1" - local files=() - while IFS= read -r -d '' f; do - local rel="${f#"$PACK_ROOT"/}" - # eternal-saas profile excludes tcg-only modules - if [[ "$profile" == "eternal-saas" && "$rel" == "project/tcg-contract.md" ]]; then - continue - fi - files+=("$f") - done < <(find "$PACK_ROOT/global" "$PACK_ROOT/project" -name '*.md' -print0 2>/dev/null | sort -z) - printf '%s\n' "${files[@]}" + python3 - "$MANIFEST_SOURCE" "$PACK_ROOT" "$CURSOR_PACK_ROOT" "$PROFILE" <<'PYEOF' +import json +import re +import sys +from pathlib import Path + +manifest_path, pack_root, cursor_root, profile = sys.argv[1:] +manifest = json.loads(Path(manifest_path).read_text()) +profiles = manifest.get("profiles", {}) + +def expand(name, seen=None): + seen = seen or set() + if name in seen: + raise SystemExit(f"profile cycle detected: {name}") + if name not in profiles: + raise SystemExit(f"unknown profile: {name}") + seen.add(name) + item = profiles[name] + modules = [] + if item.get("extends"): + modules.extend(expand(item["extends"], seen)) + modules.extend(item.get("modules", [])) + return modules + +for key in expand(profile): + source = Path(pack_root) / f"{key}.md" + if not source.is_file(): + raise SystemExit(f"manifest profile {profile} references missing module: {key}") + text = source.read_text() + match = re.search(r"^id:\s*(\S+)\s*$", text, re.MULTILINE) + if not match: + raise SystemExit(f"module missing id frontmatter: {key}") + cursor = Path(cursor_root) / Path(key).parent / f"{match.group(1)}.mdc" + if not cursor.is_file(): + raise SystemExit(f"generated Cursor rule missing for module {key}: {cursor}") + print(f"{key}\t{match.group(1)}") +PYEOF } -# ── dry-run: list planned operations ─────────────────────────────────────── +cursor_rel_for() { + local key="$1" id="$2" subdir + subdir="$(dirname "$key")" + if [[ "$subdir" == "." ]]; then + printf '%s.mdc\n' "$id" + else + printf '%s/%s.mdc\n' "$subdir" "$id" + fi +} if [[ "$DRY_RUN" -eq 1 && "$CHECK_MODE" -eq 0 ]]; then echo "dry-run: profile=$PROFILE target=$TARGET" echo "planned copies to $TARGET:" - while IFS= read -r src; do - rel="${src#$PACK_ROOT/}" - dest_claude="$CLAUDE_RULES_DEST/$rel" - dest_cursor="$CURSOR_RULES_DEST/$rel" - echo " copy → .claude/rules/eternal-saas/$rel" - echo " copy → .cursor/rules/eternal-saas/$rel" - done < <(collect_modules "$PROFILE") + while IFS=$'\t' read -r key id; do + [[ -n "$key" && -n "$id" ]] || continue + rel="$key.md" + cursor_rel="$(cursor_rel_for "$key" "$id")" + echo " copy -> .claude/rules/eternal-saas/$rel" + echo " copy -> .cursor/rules/eternal-saas/$cursor_rel" + done < <(collect_modules) echo "planned receipt: $MANIFEST_RECEIPT" exit 0 fi -# ── check mode ───────────────────────────────────────────────────────────── - if [[ "$CHECK_MODE" -eq 1 ]]; then if [[ ! -f "$MANIFEST_RECEIPT" ]]; then echo "not installed: $MANIFEST_RECEIPT not found" >&2 exit 1 fi - install_ts="$(python3 -c "import json,sys; d=json.load(open('$MANIFEST_RECEIPT')); print(d.get('installedAt',''))" 2>/dev/null || echo "")" - receipt_sums="$(python3 -c "import json,sys; d=json.load(open('$MANIFEST_RECEIPT')); [print(k,v) for k,v in d.get('checksums',{}).items()]" 2>/dev/null || echo "")" + install_ts="$(receipt_installed_at)" + receipt_sums="$(receipt_checksums checksums)" + cursor_receipt_sums="$(receipt_checksums cursorChecksums)" any_stale=0 any_modified=0 - while IFS= read -r src; do - rel="${src#$PACK_ROOT/}" + while IFS=$'\t' read -r key id; do + [[ -n "$key" && -n "$id" ]] || continue + rel="$key.md" + src="$PACK_ROOT/$rel" + cursor_rel="$(cursor_rel_for "$key" "$id")" + cursor_src="$CURSOR_PACK_ROOT/$cursor_rel" dest="$CLAUDE_RULES_DEST/$rel" + cursor_dest="$CURSOR_RULES_DEST/$cursor_rel" if [[ ! -f "$dest" ]]; then echo "missing: $rel" any_stale=1 continue fi - # Check if locally modified - receipt_sum="$(echo "$receipt_sums" | grep "^$rel " | awk '{print $2}')" + receipt_sum="$(receipt_checksum_for "$receipt_sums" "$rel")" current_sum="$(file_sha256 "$dest")" if [[ -n "$receipt_sum" && "$current_sum" != "$receipt_sum" ]]; then echo "locally-modified: $rel" any_modified=1 continue fi - # Check if stale (source newer than install time) if [[ -n "$install_ts" ]]; then - src_mtime="$(python3 -c "import os; print(int(os.path.getmtime('$src')))" 2>/dev/null || echo "0")" - install_epoch="$(python3 -c "from datetime import datetime; print(int(datetime.fromisoformat('$install_ts').timestamp()))" 2>/dev/null || echo "0")" + src_mtime="$(python3 -c "import os, sys; print(int(os.path.getmtime(sys.argv[1])))" "$src" 2>/dev/null || echo "0")" + install_epoch="$(python3 -c "from datetime import datetime; import sys; value = sys.argv[1]; value = value[:-1] + '+00:00' if value.endswith('Z') else value; print(int(datetime.fromisoformat(value).timestamp()))" "$install_ts" 2>/dev/null || echo "0")" if [[ "$src_mtime" -gt "$install_epoch" ]]; then echo "stale: $rel" any_stale=1 @@ -134,16 +200,21 @@ if [[ "$CHECK_MODE" -eq 1 ]]; then fi fi echo "current: $rel" - # Also verify cursor copy - cursor_dest="$CURSOR_RULES_DEST/$rel" if [[ ! -f "$cursor_dest" ]]; then - echo "missing-cursor: $rel" + echo "missing-cursor: $cursor_rel" any_stale=1 - elif [[ "$(file_sha256 "$cursor_dest")" != "$(file_sha256 "$src")" ]]; then - echo "cursor-modified: $rel" - any_modified=1 + else + cursor_receipt_sum="$(receipt_checksum_for "$cursor_receipt_sums" "$cursor_rel")" + cursor_current_sum="$(file_sha256 "$cursor_dest")" + if [[ -n "$cursor_receipt_sum" && "$cursor_current_sum" != "$cursor_receipt_sum" ]]; then + echo "cursor-modified: $cursor_rel" + any_modified=1 + elif [[ -z "$cursor_receipt_sum" && "$cursor_current_sum" != "$(file_sha256 "$cursor_src")" ]]; then + echo "cursor-modified: $cursor_rel" + any_modified=1 + fi fi - done < <(collect_modules "$PROFILE") + done < <(collect_modules) if [[ "$any_modified" -gt 0 || "$any_stale" -gt 0 ]]; then exit 1 @@ -151,61 +222,90 @@ if [[ "$CHECK_MODE" -eq 1 ]]; then exit 0 fi -# ── install mode ──────────────────────────────────────────────────────────── - -# Check for locally-modified files when not forcing if [[ -f "$MANIFEST_RECEIPT" && "$FORCE" -eq 0 ]]; then - receipt_sums="$(python3 -c "import json,sys; d=json.load(open('$MANIFEST_RECEIPT')); [print(k,v) for k,v in d.get('checksums',{}).items()]" 2>/dev/null || echo "")" + receipt_sums="$(receipt_checksums checksums)" + cursor_receipt_sums="$(receipt_checksums cursorChecksums)" modified_files=() - while IFS= read -r src; do - rel="${src#$PACK_ROOT/}" + while IFS=$'\t' read -r key id; do + [[ -n "$key" && -n "$id" ]] || continue + rel="$key.md" + cursor_rel="$(cursor_rel_for "$key" "$id")" + cursor_src="$CURSOR_PACK_ROOT/$cursor_rel" dest="$CLAUDE_RULES_DEST/$rel" - if [[ ! -f "$dest" ]]; then continue; fi - receipt_sum="$(echo "$receipt_sums" | grep "^$rel " | awk '{print $2}')" - current_sum="$(file_sha256 "$dest")" - if [[ -n "$receipt_sum" && "$current_sum" != "$receipt_sum" ]]; then - modified_files+=("$rel") + cursor_dest="$CURSOR_RULES_DEST/$cursor_rel" + if [[ -f "$dest" ]]; then + receipt_sum="$(receipt_checksum_for "$receipt_sums" "$rel")" + current_sum="$(file_sha256 "$dest")" + if [[ -n "$receipt_sum" && "$current_sum" != "$receipt_sum" ]]; then + modified_files+=("$rel") + fi + fi + if [[ -f "$cursor_dest" ]]; then + cursor_receipt_sum="$(receipt_checksum_for "$cursor_receipt_sums" "$cursor_rel")" + cursor_current_sum="$(file_sha256 "$cursor_dest")" + if [[ -n "$cursor_receipt_sum" && "$cursor_current_sum" != "$cursor_receipt_sum" ]]; then + modified_files+=("$cursor_rel") + elif [[ -z "$cursor_receipt_sum" && "$cursor_current_sum" != "$(file_sha256 "$cursor_src")" ]]; then + modified_files+=("$cursor_rel") + fi fi - done < <(collect_modules "$PROFILE") + done < <(collect_modules) if [[ "${#modified_files[@]}" -gt 0 ]]; then echo "error: locally-modified files would be overwritten. Use --force to proceed:" >&2 - for f in "${modified_files[@]}"; do echo " $f" >&2; done + for file in "${modified_files[@]}"; do echo " $file" >&2; done exit 1 fi fi -# Install files — accumulate checksums in a temp file (bash 3 compatible) install_ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" checksums_tmp="$(mktemp)" -trap 'rm -f "$checksums_tmp"' EXIT +cursor_checksums_tmp="$(mktemp)" +trap 'rm -f "$checksums_tmp" "$cursor_checksums_tmp"' EXIT -while IFS= read -r src; do - rel="${src#$PACK_ROOT/}" +while IFS=$'\t' read -r key id; do + [[ -n "$key" && -n "$id" ]] || continue + rel="$key.md" + cursor_rel="$(cursor_rel_for "$key" "$id")" + src="$PACK_ROOT/$rel" + cursor_src="$CURSOR_PACK_ROOT/$cursor_rel" dest_claude="$CLAUDE_RULES_DEST/$rel" - dest_cursor="$CURSOR_RULES_DEST/$rel" + dest_cursor="$CURSOR_RULES_DEST/$cursor_rel" mkdir -p "$(dirname "$dest_claude")" "$(dirname "$dest_cursor")" cp "$src" "$dest_claude" - cp "$src" "$dest_cursor" + cp "$cursor_src" "$dest_cursor" sum="$(file_sha256 "$dest_claude")" + cursor_sum="$(file_sha256 "$dest_cursor")" printf '%s\t%s\n' "$rel" "$sum" >> "$checksums_tmp" + printf '%s\t%s\n' "$cursor_rel" "$cursor_sum" >> "$cursor_checksums_tmp" echo "installed: $rel" -done < <(collect_modules "$PROFILE") +done < <(collect_modules) -# Write manifest receipt via Python (handles JSON escaping correctly) mkdir -p "$(dirname "$MANIFEST_RECEIPT")" -python3 - "$MANIFEST_RECEIPT" "$PROFILE" "$install_ts" "$checksums_tmp" <<'PYEOF' +python3 - "$MANIFEST_RECEIPT" "$PROFILE" "$install_ts" "$checksums_tmp" "$cursor_checksums_tmp" <<'PYEOF' import sys, json -receipt_path, profile, installed_at, checksums_file = sys.argv[1:] +receipt_path, profile, installed_at, checksums_file, cursor_checksums_file = sys.argv[1:] checksums = {} +cursor_checksums = {} with open(checksums_file) as f: for line in f: line = line.rstrip('\n') if '\t' in line: key, val = line.split('\t', 1) checksums[key] = val -receipt = {"profile": profile, "installedAt": installed_at, "checksums": checksums} +with open(cursor_checksums_file) as f: + for line in f: + line = line.rstrip('\n') + if '\t' in line: + key, val = line.split('\t', 1) + cursor_checksums[key] = val +receipt = { + "profile": profile, + "installedAt": installed_at, + "checksums": checksums, + "cursorChecksums": cursor_checksums, +} with open(receipt_path, 'w') as out: json.dump(receipt, out, indent=2) out.write('\n') diff --git a/scripts/install.sh b/scripts/install.sh index 90a6c93..bc62aa2 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -368,7 +368,7 @@ validate_source_install_inputs() { [[ -s "$ROOT/skills/bundled/$skill/SKILL.md" ]] || missing+=("$ROOT/skills/bundled/$skill/SKILL.md") done if (( ${#missing[@]} > 0 )); then - printf 'install dry-run failed; missing source files:\n' >&2 + printf 'install failed; missing source files:\n' >&2 printf ' %s\n' "${missing[@]}" >&2 return 1 fi @@ -386,8 +386,9 @@ migrate_legacy_runtime_dir() { fi } +validate_source_install_inputs + if [[ "$DRY_RUN" == "1" ]]; then - validate_source_install_inputs printf 'Dry run: profile=%s manifest=%s\n' "$PROFILE" "$PROFILE_MANIFEST" printf 'Dry run: would validate stack profile with scripts/stack-profile-check.mjs\n' printf 'Dry run: would install Eternal Stack files into %s\n' "$TARGET" diff --git a/scripts/performance-baseline.mjs b/scripts/performance-baseline.mjs index 2c95d2e..357a15f 100644 --- a/scripts/performance-baseline.mjs +++ b/scripts/performance-baseline.mjs @@ -120,7 +120,11 @@ function readJsonFromStdin() { process.stdin.on("end", () => { clearTimeout(timer); try { - resolve(JSON.parse(input || "{}")); + if (!input.trim()) { + reject(new Error("stdin closed without JSON; pipe JSON and close stdin/EOF")); + return; + } + resolve(JSON.parse(input)); } catch (error) { reject(error); } diff --git a/scripts/plan-readiness-check.mjs b/scripts/plan-readiness-check.mjs index 644b6c6..b300fdb 100644 --- a/scripts/plan-readiness-check.mjs +++ b/scripts/plan-readiness-check.mjs @@ -255,7 +255,7 @@ const optionalMetadata = { deepStackArtifacts: /^Deep stack artifacts:\s*\S/im.test(text), }; -// Hyphen guards prevent matching hyphenated proper names such as the agency-tbd repo. +// Hyphen guards prevent matching TBD when it is part of an identifier such as my-TBD-project. forbidPattern('tbd', /(?"$email_state" -email_dry_payload="$(jq -nc '{session_id:"canary-email-triage",tool_name:"Bash",tool_input:{command:"vivaz-email triage run --account agencia --max-inbox 50"}}')" +email_dry_payload="$(jq -nc '{session_id:"canary-email-triage",tool_name:"Bash",tool_input:{command:"etrnl-email triage run --account fixture-account --max-inbox 50"}}')" email_dry_out="$(printf '%s' "$email_dry_payload" | CLAUDE_GUARD_STATE_DIR="$canary_state" "$TARGET/hooks/cc-pretooluse-guard.sh")" if ! jq -e '.hookSpecificOutput.permissionDecision == "deny"' <<<"$email_dry_out" >/dev/null; then printf 'fail: email-triage canary accepted dry triage run: %s\n' "$email_dry_out" >&2 @@ -119,9 +119,9 @@ if [[ "$email_dry_out" != *"Dry email-triage runs are blocked"* ]]; then exit 1 fi -jq '.successfulCommands = [{command:"vivaz-email triage guarded-run --account agencia --max-inbox 500 --apply --require-insights", at:"2026-01-01T00:00:01Z"}]' "$email_state" >"$email_state.tmp" +jq '.successfulCommands = [{command:"etrnl-email triage guarded-run --account fixture-account --max-inbox 500 --apply --require-insights", at:"2026-01-01T00:00:01Z"}]' "$email_state" >"$email_state.tmp" mv -- "$email_state.tmp" "$email_state" -email_queue_payload="$(jq -nc '{session_id:"canary-email-triage",tool_name:"Bash",tool_input:{command:"vivaz-email triage queue --run-id triage_canary --mode reply --format markdown --next"}}')" +email_queue_payload="$(jq -nc '{session_id:"canary-email-triage",tool_name:"Bash",tool_input:{command:"etrnl-email triage queue --run-id triage_canary --mode reply --format markdown --next"}}')" email_queue_out="$(printf '%s' "$email_queue_payload" | CLAUDE_GUARD_STATE_DIR="$canary_state" "$TARGET/hooks/cc-pretooluse-guard.sh")" if ! jq -e '.hookSpecificOutput.permissionDecision == "deny"' <<<"$email_queue_out" >/dev/null; then printf 'fail: email-triage canary accepted queue before verify: %s\n' "$email_queue_out" >&2 @@ -132,13 +132,13 @@ if [[ "$email_queue_out" != *"queue is blocked until Inbox Zero verification"* ] exit 1 fi -canary_vivaz_email="$canary_tmp/vivaz-email" -cat >"$canary_vivaz_email" <<'BASH' +canary_etrnl_email="$canary_tmp/etrnl-email" +cat >"$canary_etrnl_email" <<'BASH' #!/usr/bin/env bash if [[ "$1 $2" == "triage verify" ]]; then - if [[ "${VIVAZ_EMAIL_VERIFY_NONZERO:-0}" == "1" ]]; then + if [[ "${ETRNL_EMAIL_VERIFY_NONZERO:-0}" == "1" ]]; then printf '{"ok":true,"data":{"verified":true,"dry_run":false,"gmail_mutated":true,"inbox_zero_verified":true,"inbox_count":1}}\n' - elif [[ "${VIVAZ_EMAIL_VERIFY_READY:-0}" == "1" ]]; then + elif [[ "${ETRNL_EMAIL_VERIFY_READY:-0}" == "1" ]]; then printf '{"ok":true,"data":{"verified":true,"dry_run":true,"gmail_mutated":false,"inbox_zero_verified":true,"queue_ready_without_mutation":true,"inbox_count":0,"action_backlog_count":31}}\n' else printf '{"ok":true,"data":{"verified":true,"dry_run":false,"gmail_mutated":true,"inbox_zero_verified":true,"inbox_count":0}}\n' @@ -147,25 +147,25 @@ if [[ "$1 $2" == "triage verify" ]]; then fi exit 0 BASH -chmod +x "$canary_vivaz_email" +chmod +x "$canary_etrnl_email" jq '.successfulCommands = [ - {command:"vivaz-email triage guarded-run --account agencia --max-inbox 500 --apply --require-insights", at:"2026-01-01T00:00:01Z"}, - {command:"vivaz-email triage verify --latest --account agencia", at:"2026-01-01T00:00:02Z"} + {command:"etrnl-email triage guarded-run --account fixture-account --max-inbox 500 --apply --require-insights", at:"2026-01-01T00:00:01Z"}, + {command:"etrnl-email triage verify --latest --account fixture-account", at:"2026-01-01T00:00:02Z"} ]' "$email_state" >"$email_state.tmp" mv -- "$email_state.tmp" "$email_state" -email_queue_verified_out="$(printf '%s' "$email_queue_payload" | VIVAZ_EMAIL_BIN="$canary_vivaz_email" CLAUDE_GUARD_STATE_DIR="$canary_state" "$TARGET/hooks/cc-pretooluse-guard.sh")" +email_queue_verified_out="$(printf '%s' "$email_queue_payload" | ETRNL_EMAIL_BIN="$canary_etrnl_email" CLAUDE_GUARD_STATE_DIR="$canary_state" "$TARGET/hooks/cc-pretooluse-guard.sh")" if ! jq -e '.continue == true' <<<"$email_queue_verified_out" >/dev/null; then printf 'fail: email-triage canary blocked provider-verified queue: %s\n' "$email_queue_verified_out" >&2 exit 1 fi -email_queue_no_mutation_out="$(printf '%s' "$email_queue_payload" | VIVAZ_EMAIL_VERIFY_READY=1 VIVAZ_EMAIL_BIN="$canary_vivaz_email" CLAUDE_GUARD_STATE_DIR="$canary_state" "$TARGET/hooks/cc-pretooluse-guard.sh")" +email_queue_no_mutation_out="$(printf '%s' "$email_queue_payload" | ETRNL_EMAIL_VERIFY_READY=1 ETRNL_EMAIL_BIN="$canary_etrnl_email" CLAUDE_GUARD_STATE_DIR="$canary_state" "$TARGET/hooks/cc-pretooluse-guard.sh")" if ! jq -e '.continue == true' <<<"$email_queue_no_mutation_out" >/dev/null; then printf 'fail: email-triage canary blocked no-mutation ready queue: %s\n' "$email_queue_no_mutation_out" >&2 exit 1 fi -email_queue_bad_verify_out="$(printf '%s' "$email_queue_payload" | VIVAZ_EMAIL_VERIFY_NONZERO=1 VIVAZ_EMAIL_BIN="$canary_vivaz_email" CLAUDE_GUARD_STATE_DIR="$canary_state" "$TARGET/hooks/cc-pretooluse-guard.sh")" +email_queue_bad_verify_out="$(printf '%s' "$email_queue_payload" | ETRNL_EMAIL_VERIFY_NONZERO=1 ETRNL_EMAIL_BIN="$canary_etrnl_email" CLAUDE_GUARD_STATE_DIR="$canary_state" "$TARGET/hooks/cc-pretooluse-guard.sh")" if ! jq -e '.hookSpecificOutput.permissionDecision == "deny"' <<<"$email_queue_bad_verify_out" >/dev/null; then printf 'fail: email-triage canary accepted queue after nonzero provider verify: %s\n' "$email_queue_bad_verify_out" >&2 exit 1 diff --git a/scripts/pr-preflight.mjs b/scripts/pr-preflight.mjs index 25823bb..7a009d9 100644 --- a/scripts/pr-preflight.mjs +++ b/scripts/pr-preflight.mjs @@ -53,6 +53,7 @@ function emit(payload) { } function packageManagerGate() { + if (existsSync("scripts/doctor.sh")) return "scripts/doctor.sh"; if (existsSync("pnpm-lock.yaml")) return "pnpm"; if (existsSync("yarn.lock")) return "yarn"; if (existsSync("bun.lockb")) return "bun"; diff --git a/scripts/skill-contract-check.mjs b/scripts/skill-contract-check.mjs index 568ebda..015063d 100644 --- a/scripts/skill-contract-check.mjs +++ b/scripts/skill-contract-check.mjs @@ -153,11 +153,18 @@ function markdownFilesUnder(dir) { function assertDirectiveLanguage(file, text) { const relPath = path.relative(root, file) || file; const lines = text.split(/\r?\n/); + let inFence = false; for (let index = 0; index < lines.length; index += 1) { const line = lines[index]; - if (!line.trim()) continue; + if (/^\s*(```|~~~)/.test(line)) { + inFence = !inFence; + continue; + } + if (inFence) continue; + const lineForScan = line.replace(/https?:\/\/[^)\]\s]+/g, ""); + if (!lineForScan.trim()) continue; for (const entry of softDirectivePatterns) { - if (entry.pattern.test(line)) { + if (entry.pattern.test(lineForScan)) { fail(`${relPath}:${index + 1}: advisory wording "${entry.label}" is not allowed; use directive language or explicit unavailable/not-applicable/blocker wording`); break; } @@ -185,6 +192,9 @@ const skillLists = read(skillListsPath); const ownedSkills = parseBashArray(skillLists, "OWNED_SKILLS", { onError: (detail) => fail(`scripts/lib/skill-lists.sh ${detail}`), }); +const bundledSkills = parseBashArray(skillLists, "BUNDLED_SKILLS", { + onError: (detail) => fail(`scripts/lib/skill-lists.sh ${detail}`), +}); const ownedAgents = parseBashArray(skillLists, "OWNED_AGENTS", { onError: (detail) => fail(`scripts/lib/skill-lists.sh ${detail}`), }); @@ -198,12 +208,21 @@ const actualSkills = existsSync(skillsDir) ? readdirSync(skillsDir).filter((name) => name.startsWith("etrnl-") && existsSync(path.join(skillsDir, name, "SKILL.md"))).sort() : []; const ownedSet = new Set(ownedSkills); +const bundledDir = path.join(skillsDir, "bundled"); +const actualBundledSkills = existsSync(bundledDir) + ? readdirSync(bundledDir).filter((name) => existsSync(path.join(bundledDir, name, "SKILL.md"))).sort() + : []; +const bundledSet = new Set(bundledSkills); const referencedInstalledHelpers = new Set(); for (const skill of actualSkills) { if (!ownedSet.has(skill)) fail(`skills/${skill}/SKILL.md exists but is not listed in OWNED_SKILLS`); } +for (const skill of actualBundledSkills) { + if (!bundledSet.has(skill)) fail(`skills/bundled/${skill}/SKILL.md exists but is not listed in BUNDLED_SKILLS`); +} + for (const skill of ownedSkills) { const skillPath = path.join(skillsDir, skill, "SKILL.md"); assertFile(skillPath, `owned skill ${skill}`); @@ -244,6 +263,31 @@ for (const skill of ownedSkills) { } } +for (const skill of bundledSkills) { + const skillPath = path.join(skillsDir, "bundled", skill, "SKILL.md"); + assertFile(skillPath, `bundled skill ${skill}`); + if (!existsSync(skillPath)) continue; + const relSkillPath = path.relative(root, skillPath) || `skills/bundled/${skill}/SKILL.md`; + const text = read(skillPath); + assertDirectiveLanguage(skillPath, text); + assertMandatoryRulesNameEnforcement(skillPath, text); + assertNoModelRoutingFrontmatter(text, relSkillPath); + for (const referencePath of markdownFilesUnder(path.join(skillsDir, "bundled", skill, "references"))) { + const referenceText = read(referencePath); + assertDirectiveLanguage(referencePath, referenceText); + assertMandatoryRulesNameEnforcement(referencePath, referenceText); + } + const frontmatterName = skillFrontmatterName(text, relSkillPath); + if (frontmatterName !== skill) fail(`${relSkillPath}: frontmatter name is ${frontmatterName || ""}, expected ${skill}`); + if (!docsSkills.includes(`\`${skill}\``)) fail(`${relSkillPath}: docs/skills.md missing bundled skill ${skill}`); + for (const match of text.matchAll(/`?((?:\.\/)?references\/[^`<>\s]+\.md)`?/g)) { + assertFile(path.join(skillsDir, "bundled", skill, match[1].replace(/^\.\//, "")), `${skill} reference`); + } + for (const match of text.matchAll(/`?((?:\.\/)?docs\/[^`<>\s]+\.md)`?/g)) { + assertFile(path.join(root, match[1].replace(/^\.\//, "")), `${skill} docs reference`); + } +} + const autoplan = path.join(skillsDir, "etrnl-dev-autoplan", "SKILL.md"); const plan = path.join(skillsDir, "etrnl-dev-plan", "SKILL.md"); const autoplanContent = existsSync(autoplan) ? read(autoplan) : ""; diff --git a/scripts/skill-update-prompt.mjs b/scripts/skill-update-prompt.mjs index 2b20b8d..c8e44dd 100644 --- a/scripts/skill-update-prompt.mjs +++ b/scripts/skill-update-prompt.mjs @@ -121,30 +121,39 @@ function quoteForShell(value) { const bootstrapCommand = state.sourceRoot ? `bash ${quoteForShell(path.join(state.sourceRoot, "scripts", "bootstrap-tools.sh"))} install --yes` : ""; -const rawLines = []; +const actionLines = []; +const warningLines = []; if (state.localUpdateAvailable) { - rawLines.push( + actionLines.push( `ETRNL_UPDATE_AVAILABLE installed=${state.installedCommitShort || "unknown"} source=${state.sourceCommitShort || "unknown"} version=${state.sourceVersion || "unknown"} run="${updateCommand}"`, ); } if (state.remote?.updateAvailable) { - rawLines.push( + actionLines.push( `ETRNL_REMOTE_UPDATE_AVAILABLE upstream=${state.remote.upstream || "unknown"} behind=${state.remote.behind || 0} run="${updateCommand} --pull"`, ); } for (const tool of Object.values(toolStack.tools || {})) { if (tool?.updateAvailable) { - rawLines.push( + actionLines.push( `TOOL_STACK_UPDATE_AVAILABLE ${tool.id} current=${tool.currentVersion} latest=${tool.latestVersion} run="${tool.updateCommand}"`, ); } else if (tool && (tool.kind === "claude-plugin" ? tool.pluginEnabled && !tool.pluginInstalled : tool.installed === false)) { - rawLines.push(`TOOL_STACK_MISSING ${tool.id} install="${tool.installCommand}"`); + actionLines.push(`TOOL_STACK_MISSING ${tool.id} install="${tool.installCommand}"`); } } -if (state.warning) rawLines.push(`ETRNL_UPDATE_WARNING ${state.warning}`); +if (state.warning) warningLines.push(`ETRNL_UPDATE_WARNING ${state.warning}`); +if (state.sourceGitWarning) warningLines.push(`ETRNL_UPDATE_WARNING ${state.sourceGitWarning}`); const resultOk = state.ok !== false; -const promptNeeded = Boolean(state.updateAvailable) || !resultOk || rawLines.length > 0; +const promptNeeded = Boolean( + !resultOk || + state.localUpdateAvailable || + state.remote?.updateAvailable || + missingTools.length || + toolUpdates.length || + actionLines.length, +); emit({ ok: resultOk, promptNeeded, @@ -156,10 +165,11 @@ emit({ remoteUpdateAvailable: Boolean(state.remote?.updateAvailable), missingTools, toolUpdates, + warnings: warningLines, updateCommand, bootstrapCommand, summary: promptNeeded - ? `etrnl=${state.localUpdateAvailable ? "stale" : "current"} tools=${[...missingTools, ...toolUpdates].join(",") || "current"}` + ? `etrnl=${resultOk ? (state.localUpdateAvailable ? "stale" : "current") : "degraded"} tools=${[...missingTools, ...toolUpdates].join(",") || "current"}` : "", - rawUpdateOutput: rawLines.join("\n"), + rawUpdateOutput: [...actionLines, ...warningLines].join("\n"), }); diff --git a/scripts/sync-rule-exports.mjs b/scripts/sync-rule-exports.mjs old mode 100644 new mode 100755 index ca28c77..7fcae4f --- a/scripts/sync-rule-exports.mjs +++ b/scripts/sync-rule-exports.mjs @@ -1,8 +1,7 @@ #!/usr/bin/env node /** * sync-rule-exports.mjs - * Project markdown rule modules → Cursor .mdc twins. - * Validates manifest checksums and privacy banned-token gate. + * Project markdown rule modules -> Cursor .mdc twins + manifest index. * * Usage: * node scripts/sync-rule-exports.mjs [--check] @@ -11,7 +10,7 @@ import { createHash } from 'node:crypto'; import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'node:fs'; -import { resolve, dirname, join, basename } from 'node:path'; +import { resolve, dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -37,6 +36,32 @@ function sha256(content) { return createHash('sha256').update(content).digest('hex'); } +function parseInlineArray(value) { + const items = []; + let current = ''; + let quote = ''; + for (const char of value) { + if ((char === '"' || char === "'") && !quote) { + quote = char; + current += char; + continue; + } + if (char === quote) { + quote = ''; + current += char; + continue; + } + if (char === ',' && !quote) { + items.push(current.trim().replace(/^["']|["']$/g, '')); + current = ''; + continue; + } + current += char; + } + if (current.trim()) items.push(current.trim().replace(/^["']|["']$/g, '')); + return items; +} + function parseFrontmatter(content) { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!match) return { frontmatter: {}, body: content }; @@ -67,7 +92,7 @@ function parseFrontmatter(content) { continue; } if (rest.startsWith('[') && rest.endsWith(']')) { - frontmatter[key] = rest.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')); + frontmatter[key] = parseInlineArray(rest.slice(1, -1)); } else if (rest === 'true') { frontmatter[key] = true; } else if (rest === 'false') { @@ -76,40 +101,94 @@ function parseFrontmatter(content) { frontmatter[key] = rest.replace(/^["']|["']$/g, ''); } } - if (inList && currentKey) { - frontmatter[currentKey] = [...listValues]; - } + if (inList && currentKey) frontmatter[currentKey] = [...listValues]; + + return { frontmatter, body: content.slice(match[0].length).trimStart() }; +} - const body = content.slice(match[0].length).trimStart(); - return { frontmatter, body }; +function arrayValue(value) { + if (Array.isArray(value)) return value; + return value ? [value] : []; } function buildMdcContent(frontmatter, body) { - const globs = Array.isArray(frontmatter.globs) ? frontmatter.globs : [frontmatter.globs].filter(Boolean); + const globLines = arrayValue(frontmatter.globs).map((glob) => ` - "${glob}"`).join('\n'); const description = (frontmatter.description || '').replace(/"/g, '\\"'); const alwaysApply = frontmatter.alwaysApply ?? false; - - const globLines = globs.map(g => ` - "${g}"`).join('\n'); - - return `--- -globs: -${globLines} -alwaysApply: ${alwaysApply} -description: "${description}" ---- - -${body}`; + return `---\nglobs:\n${globLines}\nalwaysApply: ${alwaysApply}\ndescription: "${description}"\n---\n\n${body}`; } function checkBannedTokens(content, bannedTokens) { - return bannedTokens.filter(token => - content.toLowerCase().includes(token.toLowerCase()) - ); + return bannedTokens.filter((token) => content.toLowerCase().includes(String(token).toLowerCase())); } function loadManifest(manifestPath) { if (!existsSync(manifestPath)) throw new Error(`Manifest not found: ${manifestPath}`); - return JSON.parse(readFileSync(manifestPath, 'utf8')); + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + const bannedTokens = privacyTokens(manifest, dirname(manifestPath)); + Object.defineProperty(manifest, '_privacyTokens', { + value: bannedTokens, + enumerable: false, + }); + return manifest; +} + +function localPrivacyTokens(filePath) { + if (!existsSync(filePath)) return []; + const content = readFileSync(filePath, 'utf8'); + if (filePath.endsWith('.json')) { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) return parsed; + return arrayValue(parsed.privacy?.bannedTokens || parsed.bannedTokens); + } + return content + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')); +} + +function privacyTokens(manifest, manifestDir) { + const tokens = [...arrayValue(manifest.privacy?.bannedTokens)]; + for (const relPath of arrayValue(manifest.privacy?.localTokenFiles)) { + tokens.push(...localPrivacyTokens(resolve(manifestDir, relPath))); + } + return [...new Set(tokens.map((token) => String(token).trim()).filter(Boolean))]; +} + +function sourceRelKey(sourcePath, rulesRoot) { + return relative(rulesRoot, sourcePath).replace(/\\/g, '/').replace(/\.md$/, ''); +} + +function moduleSourcePath(key, rulesRoot) { + return join(rulesRoot, `${key}.md`); +} + +function walkMd(dir) { + const files = []; + if (!existsSync(dir)) return files; + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) files.push(...walkMd(full)); + else if (entry.endsWith('.md')) files.push(full); + } + return files.sort((left, right) => left.localeCompare(right)); +} + +function expandProfileModules(manifest, profileName, seen = new Set()) { + if (seen.has(profileName)) throw new Error(`Profile cycle detected: ${profileName}`); + const profile = manifest.profiles?.[profileName]; + if (!profile) throw new Error(`Unknown profile: ${profileName}`); + seen.add(profileName); + const inherited = profile.extends ? expandProfileModules(manifest, profile.extends, seen) : []; + return [...inherited, ...arrayValue(profile.modules)]; +} + +function expectedModuleKeys(manifest) { + const keys = new Set(); + for (const profileName of Object.keys(manifest.profiles || {})) { + for (const key of expandProfileModules(manifest, profileName)) keys.add(key); + } + return [...keys].sort(); } function processModule(sourcePath, outputDir, manifest, check) { @@ -119,23 +198,18 @@ function processModule(sourcePath, outputDir, manifest, check) { if (!frontmatter.id) throw new Error(`Missing 'id' in frontmatter: ${sourcePath}`); if (!frontmatter.globs) throw new Error(`Missing 'globs' in frontmatter: ${sourcePath}`); if (!frontmatter.description) throw new Error(`Missing 'description' in frontmatter: ${sourcePath}`); + if (!frontmatter.hosts) throw new Error(`Missing 'hosts' in frontmatter: ${sourcePath}`); - const bannedTokens = manifest.privacy?.bannedTokens ?? []; - const violations = checkBannedTokens(content, bannedTokens); + const violations = checkBannedTokens(content, manifest._privacyTokens ?? manifest.privacy?.bannedTokens ?? []); if (violations.length > 0) { - throw new Error(`Privacy violation in ${sourcePath}: banned tokens found: ${violations.join(', ')}`); + throw new Error(`Privacy violation in ${sourcePath}: banned token match count=${violations.length}`); } const mdcContent = buildMdcContent(frontmatter, body); const mdcPath = join(outputDir, `${frontmatter.id}.mdc`); - const checksum = sha256(mdcContent); - if (check) { - if (!existsSync(mdcPath)) { - throw new Error(`Drift: expected ${mdcPath} does not exist. Run without --check to generate.`); - } - const existing = readFileSync(mdcPath, 'utf8'); - if (existing !== mdcContent) { + if (!existsSync(mdcPath)) throw new Error(`Drift: expected ${mdcPath} does not exist. Run without --check to generate.`); + if (readFileSync(mdcPath, 'utf8') !== mdcContent) { throw new Error(`Drift detected in ${mdcPath}. Run without --check to regenerate.`); } } else { @@ -143,18 +217,57 @@ function processModule(sourcePath, outputDir, manifest, check) { writeFileSync(mdcPath, mdcContent, 'utf8'); } - return { id: frontmatter.id, checksum, mdcPath }; + return { + id: frontmatter.id, + paths: arrayValue(frontmatter.paths || frontmatter.globs), + globs: arrayValue(frontmatter.globs), + hosts: arrayValue(frontmatter.hosts), + maxBytes: frontmatter.maxBytes, + verify: frontmatter.verify || '', + checksum: sha256(content), + mdcChecksum: sha256(mdcContent), + mdcPath, + }; } -function walkMd(dir) { - const files = []; - if (!existsSync(dir)) return files; - for (const entry of readdirSync(dir)) { - const full = join(dir, entry); - if (statSync(full).isDirectory()) files.push(...walkMd(full)); - else if (entry.endsWith('.md')) files.push(full); +function moduleProfiles(manifest, key) { + return Object.keys(manifest.profiles || {}) + .filter((profileName) => expandProfileModules(manifest, profileName).includes(key)) + .sort(); +} + +function buildIndex(manifest, moduleResults, previousModules) { + const modules = {}; + for (const item of moduleResults) { + const profiles = moduleProfiles(manifest, item.key); + const previous = previousModules?.[item.key] || {}; + modules[item.key] = { + id: item.result.id, + paths: item.result.paths, + globs: item.result.globs, + hosts: item.result.hosts, + profile: profiles, + verify: item.result.verify, + checksum: item.result.checksum, + mdcChecksum: item.result.mdcChecksum, + cursorPath: relative(ROOT, item.result.mdcPath).replace(/\\/g, '/'), + generatedAt: previous.generatedAt || new Date().toISOString(), + }; + if (item.result.maxBytes !== undefined) modules[item.key].maxBytes = item.result.maxBytes; + } + return modules; +} + +function canonicalManifest(manifest, modules) { + return `${JSON.stringify({ ...manifest, modules }, null, 2)}\n`; +} + +function assertManifestIndex(manifestPath, manifest, modules) { + const expected = canonicalManifest(manifest, modules); + const actual = readFileSync(manifestPath, 'utf8'); + if (actual !== expected) { + throw new Error(`Drift detected in ${relative(ROOT, manifestPath)} modules index. Run node scripts/sync-rule-exports.mjs to regenerate.`); } - return files; } const errors = []; @@ -178,22 +291,53 @@ if (singleSource) { const manifest = loadManifest(manifestPath); const rulesRoot = join(ROOT, 'rules', 'eternal-saas'); const cursorOutputRoot = join(ROOT, 'templates', 'cursor', 'rules', 'eternal-saas'); - + const expectedKeys = expectedModuleKeys(manifest); const moduleFiles = walkMd(rulesRoot); - for (const filePath of moduleFiles) { - const rel = filePath.replace(rulesRoot + '/', ''); - const subDir = dirname(rel); - const outputDir = subDir === '.' ? cursorOutputRoot : join(cursorOutputRoot, subDir); + const fileKeys = new Set(moduleFiles.map((filePath) => sourceRelKey(filePath, rulesRoot))); + + for (const key of expectedKeys) { + if (!fileKeys.has(key)) errors.push(`${key}: manifest profile references missing module source`); + } + for (const key of fileKeys) { + if (!expectedKeys.includes(key)) errors.push(`${key}: module source is not referenced by any manifest profile`); + } + + const moduleResults = []; + if (errors.length === 0) { + for (const key of expectedKeys) { + const filePath = moduleSourcePath(key, rulesRoot); + const rel = `${key}.md`; + const subDir = dirname(rel); + const outputDir = subDir === '.' ? cursorOutputRoot : join(cursorOutputRoot, subDir); + try { + const result = processModule(filePath, outputDir, manifest, checkMode); + moduleResults.push({ key, result }); + console.log(checkMode ? `ok: ${rel}` : `Generated: ${result.mdcPath}`); + } catch (err) { + errors.push(`${rel}: ${err.message}`); + exitCode = 1; + } + } + } + + if (errors.length === 0) { try { - const result = processModule(filePath, outputDir, manifest, checkMode); - console.log(checkMode ? `ok: ${rel}` : `Generated: ${result.mdcPath}`); + const modules = buildIndex(manifest, moduleResults, manifest.modules || {}); + const cleanManifest = { ...manifest, modules: {} }; + if (checkMode) { + assertManifestIndex(manifestPath, cleanManifest, modules); + } else { + writeFileSync(manifestPath, canonicalManifest(cleanManifest, modules), 'utf8'); + } } catch (err) { - errors.push(`${rel}: ${err.message}`); + errors.push(err.message); exitCode = 1; } } + if (errors.length > 0) { - for (const e of errors) console.error(`fail: ${e}`); + for (const error of errors) console.error(`fail: ${error}`); + exitCode = 1; } } diff --git a/scripts/tool-stack-check.mjs b/scripts/tool-stack-check.mjs index 965a622..815c6c5 100755 --- a/scripts/tool-stack-check.mjs +++ b/scripts/tool-stack-check.mjs @@ -24,6 +24,10 @@ const explainMode = hasFlag("--explain"); const force = hasFlag("--force"); const projectPath = valueAfter("--project", valueAfter("--cwd", "")); const hindsightStrictChecks = /^(1|true|yes)$/i.test(process.env.HINDSIGHT_STRICT_CHECKS || ""); +const toolSpecs = { + codegraph: process.env.ETRNL_CODEGRAPH_NPM_SPEC || "@colbymchenry/codegraph@1.0.1", + beads: process.env.ETRNL_BEADS_NPM_SPEC || "@beads/bd@1.0.5", +}; function usage() { console.error("usage: tool-stack-check.mjs [--json|--explain] [--force] [--project ]"); @@ -426,8 +430,8 @@ const tools = [ command: "codegraph", versionArgs: ["--version"], latest: () => npmLatest("@colbymchenry/codegraph"), - installCommand: "npm install -g @colbymchenry/codegraph && codegraph install --target all --location global --yes", - updateCommand: "npm install -g @colbymchenry/codegraph && codegraph install --target all --location global --yes", + installCommand: `npm install -g ${toolSpecs.codegraph} && codegraph install --target all --location global --yes`, + updateCommand: `npm install -g ${toolSpecs.codegraph} && codegraph install --target all --location global --yes`, healthCommand: "codegraph --version && codegraph install --print-config codex", }, { @@ -435,8 +439,8 @@ const tools = [ command: "bd", versionArgs: ["version"], latest: () => npmLatestWithFallback("@beads/bd", () => brewLatest("beads")), - installCommand: "npm install -g @beads/bd", - updateCommand: "npm install -g @beads/bd", + installCommand: `npm install -g ${toolSpecs.beads}`, + updateCommand: `npm install -g ${toolSpecs.beads}`, healthCommand: "bd version && bd status --json", }, ]; diff --git a/scripts/update-check.mjs b/scripts/update-check.mjs index c5a09b3..d117151 100755 --- a/scripts/update-check.mjs +++ b/scripts/update-check.mjs @@ -180,6 +180,10 @@ const sourceState = (root) => { const branch = branchResult.ok && branchResult.stdout ? branchResult.stdout : "unknown"; const upstreamResult = git(root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]); const dirtyResult = git(root, ["status", "--porcelain"]); + const sourceGitWarning = [ + commitResult.ok ? "" : (commitResult.stderr || commitResult.stdout || "git metadata unavailable"), + dirtyResult.ok ? "" : (dirtyResult.stderr || dirtyResult.stdout || "git dirty-state unavailable"), + ].filter(Boolean).join("; "); return { sourceRoot: root, sourceCommit: commit, @@ -188,9 +192,7 @@ const sourceState = (root) => { sourceUpstream: upstreamResult.ok ? upstreamResult.stdout : "", sourceDirty: dirtyResult.ok ? dirtyResult.stdout.length > 0 : false, sourceGitAvailable: commitResult.ok && dirtyResult.ok, - sourceGitWarning: commitResult.ok - ? "" - : commitResult.stderr || commitResult.stdout || "git metadata unavailable", + sourceGitWarning, sourceFingerprint: fingerprintSource(root), sourceVersion: sourceVersion(root), }; @@ -529,6 +531,8 @@ let autoSucceeded = false; const allowDirtyAutoUpdate = process.env.ETRNL_AUTO_UPDATE_DIRTY === "1"; if (autoEnabled && localUpdateAvailable && source.sourceDirty && !allowDirtyAutoUpdate) { autoUpdate = "ETRNL_AUTO_UPDATE_SKIPPED dirty-source-checkout (set ETRNL_AUTO_UPDATE_DIRTY=1 to override)"; +} else if (autoEnabled && localUpdateAvailable && !source.sourceGitAvailable) { + autoUpdate = `ETRNL_AUTO_UPDATE_SKIPPED untrusted-source-git-state${source.sourceGitWarning ? `: ${source.sourceGitWarning}` : ""}`; } else if (autoEnabled && localUpdateAvailable) { const updateScriptPath = path.join(root, "scripts", "update.sh"); if (!fs.existsSync(updateScriptPath)) { diff --git a/skills/bundled/abacatepay-integration/SKILL.md b/skills/bundled/abacatepay-integration/SKILL.md index a9e5c12..cb8a737 100644 --- a/skills/bundled/abacatepay-integration/SKILL.md +++ b/skills/bundled/abacatepay-integration/SKILL.md @@ -457,7 +457,7 @@ curl -X POST https://api.abacatepay.com/v1/test/pix/simulate \ ✅ **Verify webhook signatures** - Prevent fake webhooks ✅ **Idempotency** - Check if payment already processed -✅ **Handle duplicates** - Webhooks may be sent multiple times +✅ **Handle duplicates** - Webhooks can be sent multiple times ✅ **Log all events** - For debugging and auditing ✅ **Retry logic** - For failed API calls ✅ **Test mode first** - Always test before production diff --git a/skills/bundled/better-auth/SKILL.md b/skills/bundled/better-auth/SKILL.md index 338f4c7..c0f3f03 100644 --- a/skills/bundled/better-auth/SKILL.md +++ b/skills/bundled/better-auth/SKILL.md @@ -28,7 +28,7 @@ Only define `baseURL`/`secret` in config if env vars are NOT set. CLI looks for | Option | Notes | | ------------------ | --------------------------------------------- | -| `appName` | Optional display name | +| `appName` | Opt-in display name | | `baseURL` | Only if `BETTER_AUTH_URL` not set | | `basePath` | Default `/api/auth`. Set `/` for root. | | `secret` | Only if `BETTER_AUTH_SECRET` not set | @@ -131,15 +131,6 @@ When working on auth, delegate to: - **TanStack Start**: - **Expo**: -## References - -- [Database Adapters](references/database-adapters.md) - Drizzle, Kysely, Prisma adapters, Cloudflare Workers factory pattern -- [Session Management](references/sessions.md) - Cookie cache, stateless sessions, storage priority, freshAge constraints -- [Plugins and Social Auth](references/plugins.md) - Plugin setup, OAuth 2.1 provider, admin RBAC, social provider scopes -- [Email and Password](references/email-password.md) - Verification, password reset, timing attack prevention, hashing (scrypt, argon2), token security -- [Two-Factor Authentication](references/two-factor.md) - TOTP, email/SMS OTP, backup codes, trusted devices, 2FA session flow -- [Organizations](references/organizations.md) - Multi-tenant orgs, teams, invitations, RBAC, dynamic access control, lifecycle hooks -- [Configuration](references/configuration.md) - User/account config, rate limiting, hooks, CSRF, trusted origins, cookie/OAuth security, production checklist -- [Framework Integration](references/frameworks.md) - TanStack Start setup, Expo/React Native, client imports, type safety -- [Migration Guides](references/migration-guides.md) - Migrate from NextAuth/Auth.js, Clerk, or Supabase Auth with schema mappings and session strategies -- [Troubleshooting](references/troubleshooting.md) - D1 consistency, CORS, OAuth redirect, admin 403, nanostore refresh, known bugs +## Topic References + +This bundle intentionally vendors a single compact skill file. Use the official Better Auth docs above for adapter, session, plugin, email/password, 2FA, organization, framework, migration, and troubleshooting details. diff --git a/skills/bundled/brooks-audit/SKILL.md b/skills/bundled/brooks-audit/SKILL.md index 3d0cc88..ce444ab 100644 --- a/skills/bundled/brooks-audit/SKILL.md +++ b/skills/bundled/brooks-audit/SKILL.md @@ -6,7 +6,7 @@ description: > Triggers when: user asks to audit architecture, review folder/module structure, check for circular imports, understand how the codebase is organized, or asks "does this follow clean architecture?", "why does everything depend on everything?", - "are our layers correct?", "where should this code live?". + "are our layers correct?", "where must this code live?". Also triggers for onboarding requests: "explain this codebase to a new developer" or "give me a codebase tour" (use onboarding mode). Do NOT trigger for: PR-level code review (use brooks-review) or line-level refactoring diff --git a/skills/bundled/brooks-audit/architecture-guide.md b/skills/bundled/brooks-audit/architecture-guide.md index a13daa6..10cc1fc 100644 --- a/skills/bundled/brooks-audit/architecture-guide.md +++ b/skills/bundled/brooks-audit/architecture-guide.md @@ -103,7 +103,7 @@ Rules: 3. **Edges** - Solid arrows (`-->`) point FROM the depending module TO the dependency; use dotted arrows with label (`-.->|circular|`) for circular dependencies. If no circular dependencies exist, use only solid arrows 4. **Node limit** - Keep the graph to ~50 nodes maximum; collapse low-risk leaf modules into their parent if needed 5. **Fan-out** - For any node with fan-out > 5, use a descriptive label: `HighFanOutModule["ModuleName (fan-out: 7)"]` -6. **Colors** - Apply `classDef` colors AFTER completing Steps 2-4: `critical` (red `#ff6b6b`) for nodes with Critical findings, `warning` (yellow `#ffd43b`) for Warning findings, `clean` (green `#51cf66`) for nodes with no findings or only Suggestions. If no findings at all, classify all nodes as `clean` +6. **Colors** - Apply `classDef` colors AFTER completing Steps 2-4: `critical` (red `#ff6b6b`) for nodes with Critical findings, `warning` (yellow `#ffd43b`) for Warning findings, `clean` (green `#51cf66`) for nodes with no findings or only Rules. If no findings at all, classify all nodes as `clean` 7. **Direction** - Default to `graph TD` (top-down); use `graph LR` only if the architecture is clearly a left-to-right pipeline ### Step 2: Scan for Dependency Disorder diff --git a/skills/bundled/brooks-audit/onboarding-guide.md b/skills/bundled/brooks-audit/onboarding-guide.md index a32cc50..87b2d1d 100644 --- a/skills/bundled/brooks-audit/onboarding-guide.md +++ b/skills/bundled/brooks-audit/onboarding-guide.md @@ -16,7 +16,7 @@ report - no Health Score, no Iron Law findings. Focus on explanation and orienta ### Step 2: Draw the Dependency Map Draw the same Mermaid dependency graph as architecture audit Step 1, but color nodes by -**recommended reading order** using a DISTINCT palette from the severity palette +**documented reading order** using a DISTINCT palette from the severity palette (which uses red/yellow/green). This avoids confusing "red = danger" with "red = read last": - 🔵 Blue (`#339af0`): start here - entry points, core domain @@ -55,7 +55,7 @@ them to plain-language definitions. This applies Evans's Ubiquitous Language as ### Step 6: Suggest First Tasks -Based on the dependency map, suggest 2-3 low-risk areas where a new developer could make +Based on the dependency map, suggest 2-3 low-risk areas where a new developer can make their first contribution: modules with good test coverage, clear boundaries, low coupling. --- diff --git a/skills/bundled/ci-cd/SKILL.md b/skills/bundled/ci-cd/SKILL.md index 34677a6..18c5c04 100644 --- a/skills/bundled/ci-cd/SKILL.md +++ b/skills/bundled/ci-cd/SKILL.md @@ -1,10 +1,10 @@ --- name: ci-cd -description: This skill should be used when the user asks to design, audit, debug, optimize, or harden CI/CD pipelines; mentions GitHub Actions, GitLab CI, Jenkins, deployment automation, release gates, branch protection, Docker image builds, Turborepo, Nx, monorepos, pnpm workspaces, DevSecOps, SLSA, Cosign, SBOMs, OIDC, staging, canary, blue-green, rollback, flaky tests, or slow builds. +description: This skill must be used when the user asks to design, audit, debug, optimize, or harden CI/CD pipelines; mentions GitHub Actions, GitLab CI, Jenkins, deployment automation, release gates, branch protection, Docker image builds, Turborepo, Nx, monorepos, pnpm workspaces, DevSecOps, SLSA, Cosign, SBOMs, OIDC, staging, canary, blue-green, rollback, flaky tests, or slow builds. --- # CI/CD Pipeline Engineering -Design pipelines that are deterministic, fast to diagnose, hard to bypass accidentally, and safe to deploy from. Prefer current repository/runtime evidence over generic templates. For large audits, load `references/deep-playbook.md` and run `scripts/audit_github_actions.py` before editing. +Design pipelines that are deterministic, fast to diagnose, hard to bypass accidentally, and safe to deploy from. Defaults to current repository/runtime evidence over generic templates. For large audits, load `references/deep-playbook.md` and run `scripts/audit_github_actions.py` before editing. ## Operating Model @@ -17,8 +17,8 @@ Start by mapping the repo before editing pipeline files: - **PR CI**: fast signal for review readiness. - **Main CI**: full merge-integrity gate and artifact publication. - **Deploy workflow**: explicit environment gate, health check, rollback, and production evidence. - - **Nightly/scheduled**: slow, broad, or noisy checks that should not block every edit. -4. Preserve existing required check names when possible by using an aggregate job. This avoids breaking branch protection while allowing internal fan-out. + - **Nightly/scheduled**: slow, broad, or noisy checks that must not block every edit. +4. Preserve existing required check names when available by using an aggregate job. This avoids breaking branch protection while allowing internal fan-out. 5. Verify every pipeline change with local syntax checks, a real CI run, and, for deployment changes, a real status/deploy dry run or production-safe smoke. For broad CI/CD upgrades, use this sequence: @@ -32,10 +32,10 @@ For broad CI/CD upgrades, use this sequence: - Produce the same artifact for the same commit. Avoid mutable deploy inputs unless they are resolved to an immutable digest before rollout. - Never print, commit, or document secret values. Store secrets in GitHub/GitLab/Jenkins secret stores, cloud secret managers, Vault, or sealed secrets. -- Keep CI test secrets separate from staging/production secrets. CI should not need production credentials. -- Pin external actions and base images deliberately. Prefer commit SHAs for GitHub Actions in high-trust repos and immutable image digests for production bases. +- Keep CI test secrets separate from staging/production secrets. CI must not need production credentials. +- Pin external actions and base images deliberately. Defaults to commit SHAs for GitHub Actions in high-trust repos and immutable image digests for production bases. - Avoid direct `${{ github.event.* }}` interpolation inside shell `run:` blocks. Pass event data through env vars and quote carefully. -- Do not skip failing gates to go green. Fix root causes, move unsuitable checks to the right lane, or make them advisory with an explicit plan to harden later. +- Do not skip failing gates to go green. Fix root causes, move unsuitable checks to the right lane, or make them reference with an explicit plan to harden later. - Treat flaky tests as production risks. Quarantine only with owner, ticket, expiry, and replacement coverage. ## Quality Gates @@ -53,7 +53,7 @@ Use this default order, tuned per repo: 9. Staging deploy and verification 10. Production approval, deploy, health check, monitoring, rollback -Prefer fail-fast ordering inside a job, but split independent work into parallel jobs. Do not duplicate equivalent work: for example, avoid running both `test` and `test:coverage` if coverage already executes the full unit suite. +Defaults to fail-fast ordering inside a job, but split independent work into parallel jobs. Do not duplicate equivalent work: for example, avoid running both `test` and `test:coverage` if coverage already executes the full unit suite. ## Local Hooks vs CI @@ -73,7 +73,7 @@ If hooks become slow enough that developers bypass them, move the slow check to - Use path filters for docs-only changes, package-scoped changes, and Docker-only changes, but make filters conservative. - Upload artifacts on failure for debugging: Playwright reports, coverage, logs, screenshots, built assets, and test result XML. - Use GitHub Environments for production approval, environment-scoped secrets, deployment history, and URLs. -- Prefer OIDC federation to long-lived cloud keys when deploying to AWS/GCP/Azure. +- Use OIDC federation instead of long-lived cloud keys when deploying to AWS/GCP/Azure. - Use scheduled workflows for dependency audits, full E2E, long integration suites, image scans, and stale/flaky-test reports. ## Monorepos, Turborepo, Nx, and Workspaces @@ -111,11 +111,11 @@ Measure before and after. Report the long pole and the wall-clock critical path, ## Docker and Artifact Pipelines - Build once, promote the same immutable artifact across environments. -- Tag images with commit SHA and optionally branch/latest aliases; deploy by digest or SHA tag when possible. +- Tag images with commit SHA and opt-in branch/latest aliases; deploy by digest or SHA tag when available. - Use multi-stage Dockerfiles, copy dependency manifests before source for layer caching, and keep `.dockerignore` tight. - Run as non-root, set health checks where appropriate, avoid secrets in `ARG`/`ENV`, and avoid `:latest` base images in production. - Use BuildKit cache (`cache-from`/`cache-to`) and keep cache scopes stable but not over-broad. -- Scan images with tools such as Trivy/Grype; start advisory if noisy, then make high/critical exploitable findings blocking. +- Scan images with tools such as Trivy/Grype. Start in reference-only mode when a legacy image is noisy, then make confirmed high/critical exploitable findings blocking once the initial baseline is triaged. - Generate SBOMs and provenance for release artifacts when the project is production-facing. ## DevSecOps and Supply Chain @@ -156,9 +156,9 @@ For Terraform, CloudFormation, Pulumi, Kubernetes, Helm, ArgoCD, or Flux: - Run format/validate/plan in PRs. - Keep apply/sync behind protected environments. -- Upload plans as artifacts when useful, but do not leak secrets. +- Upload plans as artifacts when needed, but do not leak secrets. - Detect drift on a schedule. -- Prefer GitOps reconciliation for Kubernetes when a controller is already in use. +- Defaults to GitOps reconciliation for Kubernetes when a controller is already in use. - Validate manifests with schema/policy tools before deploy. ## Branch Protection and Release Policy diff --git a/skills/bundled/ci-cd/evals/prompts.md b/skills/bundled/ci-cd/evals/prompts.md index 639f2f5..a2514b3 100644 --- a/skills/bundled/ci-cd/evals/prompts.md +++ b/skills/bundled/ci-cd/evals/prompts.md @@ -16,4 +16,4 @@ Production deploy is manual to a VPS. Ask the agent to automate deployment safel ## Supply Chain -Repo uses broad `permissions`, unpinned third-party actions, and cloud deploy keys in secrets. Ask the agent to harden CI/CD. Expected behavior: least privilege, action pinning/update path, OIDC where possible, `pull_request_target` review, CODEOWNERS for workflows, SBOM/provenance/signing when release artifacts exist. +Repo uses broad `permissions`, unpinned third-party actions, and cloud deploy keys in secrets. Ask the agent to harden CI/CD. Expected behavior: least privilege, action pinning/update path, OIDC where available, `pull_request_target` only with explicit review and no untrusted PR code running with secrets, CODEOWNERS for workflows, SBOM/provenance/signing when release artifacts exist. diff --git a/skills/bundled/ci-cd/references/deep-playbook.md b/skills/bundled/ci-cd/references/deep-playbook.md index 3d65bd6..7101255 100644 --- a/skills/bundled/ci-cd/references/deep-playbook.md +++ b/skills/bundled/ci-cd/references/deep-playbook.md @@ -13,9 +13,9 @@ Use lanes rather than one giant workflow: | PR fast CI | pull request | yes | static checks, affected unit tests, affected build, smoke E2E | | Main CI | push to protected branch | yes | full graph tests, coverage thresholds, production build, artifact creation | | Image/release | main/tag/manual | yes for release | Docker build, scan, SBOM, provenance, signing, registry push | -| Staging deploy | main/manual | usually yes | deploy immutable artifact, migrations, smoke, integration/E2E | +| Staging deploy | main/manual | typically yes | deploy immutable artifact, migrations, smoke, integration/E2E | | Production deploy | manual/approval/tag | yes | environment approval, deploy, health checks, rollback, monitoring | -| Scheduled | nightly/weekly | advisory or blocking by policy | full E2E, dependency audit, image scans, drift detection, flaky reports | +| Scheduled | nightly/weekly | reference or blocking by policy | full E2E, dependency audit, image scans, drift detection, flaky reports | Keep PR CI fast enough that developers wait for it. Keep main/deploy authoritative enough that production safety does not depend on local hooks. @@ -30,7 +30,7 @@ Use this checklist for every workflow: - Avoid `pull_request_target` unless the workflow never checks out or executes untrusted PR code with secrets. - Avoid direct `${{ github.event.* }}` expansion inside shell scripts; put values in `env:` and quote shell variables. - Use GitHub Environments for production secrets, URLs, and approvals. -- Prefer OIDC to long-lived cloud provider keys. +- Use OIDC as the default alternative to long-lived cloud provider keys. - Add CODEOWNERS coverage for `.github/workflows/` in repos where workflow changes can expose secrets or deploy production. - Use Dependabot/Renovate for action updates if actions are pinned to SHAs; pinning without an update path becomes silent drift. - Upload failure artifacts: Playwright reports, logs, screenshots, coverage, test XML, Terraform plans with secrets redacted. @@ -53,7 +53,7 @@ Use this checklist for every workflow: ### Nx - Use `nx affected -t lint test build --base= --head=` in CI. -- Prefer a base SHA from the latest successful protected-branch run when available, not merely the current branch tip. +- Defaults to a base SHA from the latest successful protected-branch run when available, not merely the current branch tip. - Keep named inputs and implicit dependencies accurate for config/schema/generated-code changes. - Use remote caching with branch/fork trust boundaries. - For dependency lockfile changes, expect broad invalidation; that is a safety feature. diff --git a/skills/bundled/ci-cd/references/source-map.md b/skills/bundled/ci-cd/references/source-map.md index bc493b1..949df85 100644 --- a/skills/bundled/ci-cd/references/source-map.md +++ b/skills/bundled/ci-cd/references/source-map.md @@ -13,6 +13,6 @@ This skill intentionally synthesizes: Intentional exclusions: -- Provider-specific full tutorials for AWS, GCP, Azure, Kubernetes, Jenkins, GitLab, and CircleCI. The skill contains decision guidance; load current official docs for implementation details. +- Provider-specific full tutorials for AWS, GCP, Azure, Kubernetes, Jenkins, GitLab, and CircleCI. The skill contains decision rules; load current official docs for implementation details. - Organization-specific compliance controls. Use this skill for the baseline and add local policy references per org. - Copy-paste production workflows with real secrets or account identifiers. Templates use placeholders by design. diff --git a/skills/bundled/code-simplifier/SKILL.md b/skills/bundled/code-simplifier/SKILL.md index 06a4318..63ba417 100644 --- a/skills/bundled/code-simplifier/SKILL.md +++ b/skills/bundled/code-simplifier/SKILL.md @@ -1,6 +1,6 @@ --- name: code-simplifier -description: "Simplify and refine code for clarity, consistency, and maintainability while preserving functionality. Use when asked to \"simplify\", \"clean up\", or \"refactor\" code, after writing complex code that could benefit from simplification, or when code has grown hard to follow." +description: "Simplify and refine code for clarity, consistency, and maintainability while preserving functionality. Use when asked to \"simplify\", \"clean up\", or \"refactor\" code, after writing complex code that can benefit from simplification, or when code has grown hard to follow." version: 1.0.0 source: unknown category: workflow @@ -15,10 +15,10 @@ You will analyze recently modified code and apply refinements that: 2. **Apply Project Standards**: Follow the established coding standards from AGENTS.md including: - Use ES modules with proper import sorting and extensions - - Prefer `function` keyword over arrow functions + - Use `function` declarations for exported or top-level named functions; arrow functions remain acceptable for callbacks, closures, React hook callbacks, and inline handlers - Use explicit return type annotations for top-level functions - Follow proper React component patterns with explicit Props types - - Use proper error handling patterns (avoid try/catch when possible) + - Use proper error handling patterns and avoid unnecessary exception-handling blocks - Maintain consistent naming conventions 3. **Enhance Clarity**: Simplify code structure by: @@ -27,10 +27,10 @@ You will analyze recently modified code and apply refinements that: - Improving readability through clear variable and function names - Consolidating related logic - Removing unnecessary comments that describe obvious code - - IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions + - IMPORTANT: Avoid nested ternary operators - use switch statements or if/else chains for multiple conditions - Choose clarity over brevity - explicit code is often better than overly compact code -4. **Maintain Balance**: Avoid over-simplification that could: +4. **Maintain Balance**: Avoid over-simplification that can: - Reduce code clarity or maintainability - Create overly clever solutions that are hard to understand - Combine too many concerns into single functions or components diff --git a/skills/bundled/domain-ml/SKILL.md b/skills/bundled/domain-ml/SKILL.md index 9d28c3b..cbff363 100644 --- a/skills/bundled/domain-ml/SKILL.md +++ b/skills/bundled/domain-ml/SKILL.md @@ -71,7 +71,7 @@ From constraints to design (Layer 2): ## Use Case → Framework -| Use Case | Recommended | Why | +| Use Case | Documented | Why | |----------|-------------|-----| | Inference only | tract (ONNX) | Lightweight, portable | | Training + inference | candle, burn | Pure Rust, GPU | diff --git a/skills/bundled/eternal-best-practices/SKILL.md b/skills/bundled/eternal-best-practices/SKILL.md index ed6fbea..b7abd5c 100644 --- a/skills/bundled/eternal-best-practices/SKILL.md +++ b/skills/bundled/eternal-best-practices/SKILL.md @@ -15,6 +15,8 @@ category: workflow **Auto-invoke when editing code** - Codex hooks reference this skill for blocker-level enforcement. +Code samples use `@example-suite/core-domain` as a fictional placeholder namespace. Replace it with the project's real domain package or local domain modules. + --- ## 🏗️ Architecture Pattern: Pragmatic Hexagonal Architecture @@ -98,14 +100,14 @@ await prisma.campaign.update({ ```typescript // ✅ Use tenant-safe repository (preferred) -import { clientRepository } from '@core-suite/core-domain' +import { clientRepository } from '@example-suite/core-domain' const clients = await clientRepository.findMany(tenantId, { where: { name: 'John' } }) // ✅ Use helper for composite keys -import { findManyForTenant } from '@core-suite/core-domain/repositories' +import { findManyForTenant } from '@example-suite/core-domain/repositories' const campaigns = await findManyForTenant(prisma.campaign, tenantId, { where: { status: 'ACTIVE' }, @@ -115,7 +117,7 @@ const campaigns = await findManyForTenant(prisma.campaign, tenantId, { }) // ✅ Update with tenant-safe helper -import { updateOneForTenant } from '@core-suite/core-domain/repositories' +import { updateOneForTenant } from '@example-suite/core-domain/repositories' await updateOneForTenant(prisma.campaign, tenantId, { where: { id: campaignId }, @@ -123,7 +125,7 @@ await updateOneForTenant(prisma.campaign, tenantId, { }) ``` -**Repository Pattern (Recommended):** +**Repository Pattern (Documented):** ```typescript // packages/core-domain/src/repositories/client.repository.ts @@ -257,9 +259,11 @@ const agencyCut = campaign.totalValue * 0.20; // ❌ Imprecise + no currency #### ✅ ALWAYS (Precise + currency-aware): +`@example-suite/core-domain` is a fictional placeholder namespace in these examples. Use the project's real domain package or a local Money VO implementation. + ```typescript // ✅ Money Value Object for all financial operations -import { Money } from '@core-suite/core-domain' +import { Money } from '@example-suite/core-domain' const price = Money.fromCents(15000) // R$ 150.00 const discount = Money.fromCents(3000) // R$ 30.00 @@ -380,7 +384,7 @@ export function calculateSplits(params: { ```typescript // packages/domain/financial/calculate-splits.test.ts -import { Money } from '@core-suite/core-domain' +import { Money } from '@example-suite/core-domain' it('should calculate exact splits for R$ 1000 campaign', () => { const result = calculateSplits({ @@ -727,9 +731,9 @@ const campaigns = await orpcClient.campaigns.list({ limit: 20 }) ```typescript // apps/web/src/app/api/campaigns/route.ts -import { getTenantContext } from '@core-suite/core-auth/server' +import { getTenantContext } from '@example-suite/core-auth/server' import { successResponse, errorResponse } from '@/lib/api/response-builder' -import { CoreErrorCode } from '@core-suite/shared-constants' +import { CoreErrorCode } from '@example-suite/shared-constants' export async function GET(request: NextRequest) { const startTime = Date.now() @@ -761,7 +765,7 @@ export async function GET(request: NextRequest) { 'use server' import { revalidatePath } from 'next/cache' -import { campaignRepository } from '@core-suite/core-domain' +import { campaignRepository } from '@example-suite/core-domain' import { getTenantContextFromAuth } from '@/lib/auth-helpers' export async function createCampaign(formData: FormData) { @@ -810,7 +814,7 @@ throw new Error('not found') throw new Error('Campaign not found') ``` -#### ✅ PREFER (Centralized error codes): +#### ✅ Defaults to (Centralized error codes): ```typescript // packages/shared-constants/src/error-codes.ts @@ -836,7 +840,7 @@ export enum CoreErrorCode { } // Usage in oRPC: -import { CoreErrorCode } from '@core-suite/shared-constants' +import { CoreErrorCode } from '@example-suite/shared-constants' throw new ORPCError({ code: 'NOT_FOUND', @@ -876,7 +880,7 @@ return NextResponse.json({ }) ``` -#### ✅ PREFER (Response builders): +#### ✅ Defaults to (Response builders): ```typescript // lib/api/response-builder.ts @@ -991,11 +995,11 @@ export const campaignsRouter = router({ // ❌ How do you test this? Need to mock Prisma, session, etc. ``` -#### ✅ PREFER (Thin controller + domain): +#### ✅ Defaults to (Thin controller + domain): ```typescript // packages/domain/financial/calculate-splits.ts -import { Money } from '@core-suite/core-domain' +import { Money } from '@example-suite/core-domain' export type SplitParams = { totalValue: Money; @@ -1049,7 +1053,7 @@ export function calculateSplits(params: SplitParams): PaymentSplit { // packages/domain/financial/calculate-splits.test.ts import { describe, it, expect } from 'vitest'; -import { Money } from '@core-suite/core-domain' +import { Money } from '@example-suite/core-domain' import { calculateSplits } from './calculate-splits'; describe('calculateSplits', () => { @@ -1085,7 +1089,7 @@ describe('calculateSplits', () => { }); // packages/api/src/routers/campaigns.ts - ✅ Thin controller -import { calculateSplits } from '@agency-tbd/domain/financial/calculate-splits'; +import { calculateSplits } from '@example-suite/domain/financial/calculate-splits'; export const campaignsRouter = router({ calculatePayout: protectedProcedure @@ -1220,7 +1224,7 @@ export const env = createEnv({ }); // Usage in any file: -import { env } from "@agency-tbd/env"; +import { env } from "@example-suite/env"; const dbUrl = env.DATABASE_URL; // ✅ Type-safe, validated const apiKey = env.STRIPE_SECRET_KEY; // ✅ Validated format @@ -1229,7 +1233,7 @@ const apiKey = env.STRIPE_SECRET_KEY; // ✅ Validated format **Benefits:** - ✅ App **fails fast** at startup (not in production after deploy) - ✅ Type-safe access (`env.DATABASE_URL` autocompletes) -- ✅ Documents required vs optional vars +- ✅ Documents required vs opt-in vars - ✅ Validates formats (URLs, API key prefixes, etc.) - ✅ Environment-specific validation (prod requires certain vars) @@ -1262,7 +1266,7 @@ const client = { id: '123', name: 'Test' } as any const campaign = { id: '456' } as Campaign // Missing required fields! ``` -#### ✅ PREFER (Centralized fixtures): +#### ✅ Defaults to (Centralized fixtures): ```typescript // @/__tests__/fixtures/client.fixture.ts @@ -1364,7 +1368,7 @@ export async function updateAppointment(params: { --- -## 🟡 WARNING-Level Rules (Strong Recommendations) +## 🟡 WARNING-Level Rules (Strong Rules) ### 12. Input Validation (oRPC + Zod) @@ -1413,7 +1417,7 @@ export const campaignsRouter = router({ ```typescript // packages/api/src/middleware/tracing.ts import { trace, context, SpanStatusCode } from '@opentelemetry/api'; -import { logger } from '@agency-tbd/observability'; +import { logger } from '@example-suite/observability'; export const tracingMiddleware = t.middleware(async ({ ctx, next, path, type }) => { const tracer = trace.getTracer('agency-platform'); @@ -1574,12 +1578,12 @@ export const campaignsRouter = router({ }); ``` -#### ✅ PREFER (Async with queues): +#### ✅ Defaults to (Async with queues): ```typescript // packages/queue/src/index.ts (BullMQ setup) import { Queue } from 'bullmq'; -import { redis } from '@agency-tbd/db'; +import { redis } from '@example-suite/db'; export const emailQueue = new Queue('email', { connection: redis }); export const notaFiscalQueue = new Queue('nota-fiscal', { connection: redis }); @@ -1616,7 +1620,7 @@ export const campaignsRouter = router({ // packages/worker/src/processors/emailProcessor.ts import { Worker } from 'bullmq'; -import { sendEmail } from '@agency-tbd/email'; +import { sendEmail } from '@example-suite/email'; export const emailWorker = new Worker('email', async (job) => { const { to, subject, template, data } = job.data; @@ -1660,7 +1664,7 @@ export async function GET(req: Request) { } ``` -#### ✅ PREFER (Versioned endpoints): +#### ✅ Defaults to (Versioned endpoints): ```typescript // apps/web/src/app/api/public/v1/campaigns/route.ts @@ -1700,7 +1704,7 @@ export const CampaignV2Schema = z.object({ - ✅ Document breaking changes in changelog **Non-breaking changes (OK in same version):** -- ✅ Add optional fields +- ✅ Add opt-in fields - ✅ Add new endpoints - ✅ Improve error messages @@ -1730,7 +1734,7 @@ export const CampaignV2Schema = z.object({ } // apps/web/src/components/CampaignCard.tsx -import { prisma } from '@agency-tbd/db'; // ❌ Prisma in browser = 500KB + security leak +import { prisma } from '@example-suite/db'; // ❌ Prisma in browser = 500KB + security leak const campaigns = await prisma.campaign.findMany(); // ❌ Runs in browser! ``` @@ -1754,10 +1758,10 @@ export { prisma } from './prisma'; // Usage: // ✅ Frontend (types only) -import type { Campaign } from '@agency-tbd/db'; +import type { Campaign } from '@example-suite/db'; // ✅ Backend (client access) -import { prisma } from '@agency-tbd/db/client'; +import { prisma } from '@example-suite/db/client'; ``` **Benefits:** @@ -1888,7 +1892,7 @@ export async function instrumentAsync( } // Usage in oRPC procedures: -import { log, traceException, instrumentAsync } from '@agency-tbd/observability'; +import { log, traceException, instrumentAsync } from '@example-suite/observability'; export const campaignsRouter = router({ calculatePayout: protectedProcedure @@ -1952,7 +1956,7 @@ pnpm add @opentelemetry/api @opentelemetry/sdk-node pino pino-pretty **MANDATORY production standards for all API routes (internal and public).** -**See:** [ADR-0011: API Standards](../../../docs/adr/0011-api-standards-and-best-practices.md) for complete reference. +**See:** the API standards in this skill plus the repo-specific `AGENTS.md` for local enforcement commands. #### ✅ Rate Limiting (MANDATORY): @@ -2400,7 +2404,7 @@ const calculateCampaignFinancials = authenticatedProcedure | Lines | Action | |-------|--------| | 0-30 | ✅ OK (simple, focused function) | -| 31-45 | ⚠️ Consider extracting if it has clear sections | +| 31-45 | ⚠️ Evaluate extracting if it has clear sections | | 46-50 | 🟡 Extract before adding more code | | 51+ | 🔴 BLOCKER - Must extract immediately | @@ -2708,7 +2712,6 @@ export const createContext = async ({ req, res }) => { **References:** - [Better-Auth Documentation](https://www.better-auth.com/docs) -- [TECHNICAL-SPEC.md Section 5](../../../docs/TECHNICAL-SPEC.md#5-authentication--authorization) --- @@ -2874,10 +2877,8 @@ Before committing any code, verify: ## 📚 Documentation References -- [Technical Specification](../../../docs/TECHNICAL-SPEC.md) - Master implementation plan -- [AGENTS.md](../../../AGENTS.md) - Quick reference guide -- [Prisma Schema](../../../packages/db/prisma/schema.prisma) - Data model -- [oRPC Route Standards](../../../docs/standards/ORPC_ROUTE_STANDARDS.md) - API standards +- `AGENTS.md` - repository-specific quick reference guide +- Project-local schema, API, and technical specification files when they exist in the target repository --- diff --git a/skills/bundled/finding-duplicate-functions/SKILL.md b/skills/bundled/finding-duplicate-functions/SKILL.md index f693aed..2350809 100644 --- a/skills/bundled/finding-duplicate-functions/SKILL.md +++ b/skills/bundled/finding-duplicate-functions/SKILL.md @@ -95,7 +95,7 @@ Produces a prioritized markdown report grouped by confidence level. ### Phase 6: Human Review Review the report. For HIGH confidence duplicates: -1. Verify the recommended survivor has tests +1. Verify the documented survivor has tests 2. Update callers to use the survivor 3. Delete the duplicates 4. Run tests diff --git a/skills/bundled/finding-duplicate-functions/scripts/find-duplicates-prompt.md b/skills/bundled/finding-duplicate-functions/scripts/find-duplicates-prompt.md index b87925e..6dfc042 100644 --- a/skills/bundled/finding-duplicate-functions/scripts/find-duplicates-prompt.md +++ b/skills/bundled/finding-duplicate-functions/scripts/find-duplicates-prompt.md @@ -39,10 +39,10 @@ Return a JSON array of duplicate groups: } ], "differences": "", - "recommendation": { + "rules": { "action": "CONSOLIDATE|INVESTIGATE|KEEP_SEPARATE", "survivor": "", - "reason": "" + "reason": "" } } ] diff --git a/skills/bundled/finding-duplicate-functions/scripts/generate-report.sh b/skills/bundled/finding-duplicate-functions/scripts/generate-report.sh index a5cb334..d69fdf5 100755 --- a/skills/bundled/finding-duplicate-functions/scripts/generate-report.sh +++ b/skills/bundled/finding-duplicate-functions/scripts/generate-report.sh @@ -76,9 +76,9 @@ fi (.functions | map("- `\(.name)` in `\(.file):\(.line)`" + if .notes then " - \(.notes)" else "" end) | join("\n")) + "\n\n" + "**Differences:** \(.differences // "None - identical implementations")\n\n" + - "**Recommendation:** \(.recommendation.action)" + - (if .recommendation.survivor then " (survivor: `\(.recommendation.survivor)`)" else "" end) + - " - \(.recommendation.reason)\n\n" + + "**Recommendation:** \(.rules.action)" + + (if .rules.survivor then " (survivor: `\(.rules.survivor)`)" else "" end) + + " - \(.rules.reason)\n\n" + "---\n" ' "$f" || { echo "Error: failed to parse/render $f" >&2 @@ -103,7 +103,7 @@ fi (.functions | map("- `\(.name)` in `\(.file):\(.line)`" + if .notes then " - \(.notes)" else "" end) | join("\n")) + "\n\n" + "**Differences:** \(.differences)\n\n" + - "**Recommendation:** \(.recommendation.action) - \(.recommendation.reason)\n\n" + + "**Recommendation:** \(.rules.action) - \(.rules.reason)\n\n" + "---\n" ' "$f" || { echo "Error: failed to parse/render $f" >&2 diff --git a/skills/bundled/i18n-localization/SKILL.md b/skills/bundled/i18n-localization/SKILL.md index dfcabf6..2fab302 100644 --- a/skills/bundled/i18n-localization/SKILL.md +++ b/skills/bundled/i18n-localization/SKILL.md @@ -29,8 +29,8 @@ category: frontend | Public web app | ✅ Yes | | SaaS product | ✅ Yes | | Internal tool | ⚠️ Maybe | -| Single-region app | ⚠️ Consider future | -| Personal project | ❌ Optional | +| Single-region app | ⚠️ Evaluate future | +| Personal project | ❌ Opt-in | --- diff --git a/skills/bundled/money-vo-discipline/SKILL.md b/skills/bundled/money-vo-discipline/SKILL.md index 9c367f3..989e2b2 100644 --- a/skills/bundled/money-vo-discipline/SKILL.md +++ b/skills/bundled/money-vo-discipline/SKILL.md @@ -1,16 +1,16 @@ --- name: money-vo-discipline -description: "Money value object discipline for agency-tbd - all monetary values must use the Money VO from @agency-tbd/domain, never raw numbers. BRL (R$) primary currency." +description: "Money value object discipline for example-agency - all monetary values must use a project-owned Money VO, never raw numbers. BRL (R$) primary currency." version: 1.0.0 source: unknown category: backend -tags: [money, value-object, currency, brl, prices, amounts, fees, budgets, deals, prisma-json, formatters, agency-tbd, critical] +tags: [money, value-object, currency, brl, prices, amounts, fees, budgets, deals, prisma-json, formatters, example-agency, critical] --- -# Money Value Object Discipline - agency-tbd +# Money Value Object Discipline - example-agency ## Purpose -Prevent floating-point arithmetic bugs and inconsistent formatting by enforcing use of the `Money` value object from `@agency-tbd/domain` for every monetary value in the codebase. Raw `number` must never be used to represent money. +Prevent floating-point arithmetic bugs and inconsistent formatting by enforcing use of a project-owned `Money` value object for every monetary value in the codebase. Raw `number` must never be used to represent money. ## The Rule @@ -18,8 +18,10 @@ Prevent floating-point arithmetic bugs and inconsistent formatting by enforcing ## Package +`@example-suite/core-domain` is a fictional placeholder namespace used in these examples. Replace it with the project's real domain package or implement the Money VO in a local domain module before copying these imports. + ```typescript -import { Money } from '@agency-tbd/domain'; +import { Money } from '@example-suite/core-domain'; ``` ## Core Operations @@ -57,7 +59,7 @@ formatMoney(price); // "R$ 10,00" Prisma stores `Json` columns as `unknown`. When a Money value is stored as JSON, cast with double assertion: ```typescript -import { Money, type MoneyJSON } from '@agency-tbd/domain'; +import { Money, type MoneyJSON } from '@example-suite/core-domain'; // Reading from Prisma const raw = deal.dealValue as unknown as MoneyJSON; @@ -94,7 +96,7 @@ function applyFee(amountInCents: number) { /* ... */ } ## Correct End-to-End Example ```typescript -import { Money, type MoneyJSON } from '@agency-tbd/domain'; +import { Money, type MoneyJSON } from '@example-suite/core-domain'; import { formatMoney } from '~/lib/formatters'; // Handler receives budget as cents integer from validated input diff --git a/skills/bundled/orpc-patterns/SKILL.md b/skills/bundled/orpc-patterns/SKILL.md index 0392b87..ba6bdc3 100644 --- a/skills/bundled/orpc-patterns/SKILL.md +++ b/skills/bundled/orpc-patterns/SKILL.md @@ -1,11 +1,11 @@ --- -name: orpc-fullstack -description: Provides oRPC 1.x TypeScript API architecture and implementation guidance, refreshed for v1.14+. Use when building, reviewing, or modifying code that imports @orpc/*, mentions oRPC, type-safe RPC, OpenAPI-backed RPC contracts, RPC procedures, server route handlers, Hono/Next.js/TanStack Start adapters, TanStack Query clients, React Server Actions, AI SDK tools or streams, Better Auth middleware, WebSocket or event iterator features, file upload/download, CORS, observability, or oRPC security and migration decisions. +name: orpc-patterns +description: Provides oRPC 1.x TypeScript API architecture and implementation rules, refreshed for v1.14+. Use when building, reviewing, or modifying code that imports @orpc/*, mentions oRPC, type-safe RPC, OpenAPI-backed RPC contracts, RPC procedures, server route handlers, Hono/Next.js/TanStack Start adapters, TanStack Query clients, React Server Actions, AI SDK tools or streams, Better Auth middleware, WebSocket or event iterator features, file upload/download, CORS, observability, or oRPC security and migration decisions. --- -# oRPC Fullstack +# oRPC Patterns -Use this skill to build and review oRPC APIs with current v1.14+ patterns. Prefer the project's existing oRPC layout before introducing new folders or abstractions. +Use this skill to build and review oRPC APIs with current v1.14+ patterns. Defaults to the project's existing oRPC layout before introducing new folders or abstractions. ## Workflow diff --git a/skills/bundled/orpc-patterns/agents/openai.yaml b/skills/bundled/orpc-patterns/agents/openai.yaml index d72635b..abf75bd 100644 --- a/skills/bundled/orpc-patterns/agents/openai.yaml +++ b/skills/bundled/orpc-patterns/agents/openai.yaml @@ -1,6 +1,6 @@ interface: - display_name: "oRPC Fullstack" + display_name: "oRPC Patterns" short_description: "Build and review oRPC APIs" - default_prompt: "Use $orpc-fullstack to review this oRPC API change for current patterns, missing validation, typed errors, and client/query integration issues." + default_prompt: "Use $orpc-patterns to review this oRPC API change for current patterns, missing validation, typed errors, and client/query integration issues." policy: allow_implicit_invocation: true diff --git a/skills/bundled/orpc-patterns/references/client-links.md b/skills/bundled/orpc-patterns/references/client-links.md index 322a11b..17431e8 100644 --- a/skills/bundled/orpc-patterns/references/client-links.md +++ b/skills/bundled/orpc-patterns/references/client-links.md @@ -22,7 +22,7 @@ export const client: RouterClient = createORPCClie export const safeClient = createSafeClient(client); ``` -Use `safeClient` when a codebase prefers tuple errors broadly. Use normal clients with `safe(...)` only at specific call sites when throwing remains the project default. +Use `safeClient` when a codebase defaults to tuple errors broadly. Use normal clients with `safe(...)` only at specific call sites when throwing remains the project default. ## Client Context @@ -40,7 +40,7 @@ import type { ContractRouterClient } from "@orpc/contract"; export const client: ContractRouterClient = createORPCClient(link); ``` -Use `inferRPCMethodFromContractRouter(contract)` when method should follow contract metadata. +Use `inferRPCMethodFromContractRouter(contract)` when method must follow contract metadata. ## Other Links diff --git a/skills/bundled/orpc-patterns/references/contracts.md b/skills/bundled/orpc-patterns/references/contracts.md index 8c233c8..96f0fbe 100644 --- a/skills/bundled/orpc-patterns/references/contracts.md +++ b/skills/bundled/orpc-patterns/references/contracts.md @@ -67,4 +67,4 @@ Use OpenAPI-to-contract when an external OpenAPI spec is authoritative. Review g - Shared contract package: schemas, contracts, public types. - Service package: implementations, context, auth, data access. - Client package/app: links, query utilities, hydration, safe-client policy. -- Avoid runtime-importing server implementations into browser bundles; use type-only imports where possible. +- Avoid runtime-importing server implementations into browser bundles; use type-only imports where available. diff --git a/skills/bundled/orpc-patterns/references/getting-started.md b/skills/bundled/orpc-patterns/references/getting-started.md index 4a0ef27..528beba 100644 --- a/skills/bundled/orpc-patterns/references/getting-started.md +++ b/skills/bundled/orpc-patterns/references/getting-started.md @@ -2,9 +2,9 @@ ## Prerequisites -- Node.js 18+ works; Node.js 20+ is preferred for new projects. +- Node.js 18+ works; Node.js 20+ is default for new projects. - Bun, Deno, and Cloudflare Workers are supported through adapters. -- TypeScript strict mode is recommended. +- TypeScript strict mode is documented. - oRPC supports Zod, Valibot, Arktype, and any Standard Schema implementation. ## Install @@ -13,7 +13,7 @@ pnpm add @orpc/server @orpc/client ``` -Add optional packages only when needed: +Add opt-in packages only when needed: ```sh pnpm add @orpc/contract @orpc/openapi @orpc/tanstack-query @orpc/otel diff --git a/skills/bundled/orpc-patterns/references/integrations-observability.md b/skills/bundled/orpc-patterns/references/integrations-observability.md index 55fd2d7..6a79442 100644 --- a/skills/bundled/orpc-patterns/references/integrations-observability.md +++ b/skills/bundled/orpc-patterns/references/integrations-observability.md @@ -50,7 +50,7 @@ Use Pino integration or central interceptors for structured logs. Include proced ## React SWR And Pinia Colada -Official integrations exist. Prefer them over hand-rolled cache wrappers if a project uses SWR or Pinia Colada instead of TanStack Query. +Official integrations exist. Defaults to them over hand-rolled cache wrappers if a project uses SWR or Pinia Colada instead of TanStack Query. ## Rate Limit diff --git a/skills/bundled/orpc-patterns/references/middleware-context.md b/skills/bundled/orpc-patterns/references/middleware-context.md index 4b59f9f..a2da144 100644 --- a/skills/bundled/orpc-patterns/references/middleware-context.md +++ b/skills/bundled/orpc-patterns/references/middleware-context.md @@ -58,4 +58,4 @@ export const updateProject = protectedProcedure ## Lifecycle Hooks -Use oRPC lifecycle helpers/interceptors such as start, success, error, and finish handling for logging, metrics, and cleanup. Prefer central interceptors for cross-cutting concerns over handler-local logging. +Use oRPC lifecycle helpers/interceptors such as start, success, error, and finish handling for logging, metrics, and cleanup. Defaults to central interceptors for cross-cutting concerns over handler-local logging. diff --git a/skills/bundled/orpc-patterns/references/migrations-cookbook.md b/skills/bundled/orpc-patterns/references/migrations-cookbook.md index ef19ff3..c147aab 100644 --- a/skills/bundled/orpc-patterns/references/migrations-cookbook.md +++ b/skills/bundled/orpc-patterns/references/migrations-cookbook.md @@ -18,7 +18,7 @@ - Keep plain Server Actions for small UI-local mutations. - Move to oRPC when the mutation needs typed reuse, public API shape, OpenAPI, tests against a stable contract, or AI-agent-readable schemas. -- Use `.actionable()` when one procedure should support both paths. +- Use `.actionable()` when one procedure must support both paths. ## Form Validation @@ -46,7 +46,7 @@ return data; ## Common Review Nits - Missing `.output(...)` on public procedures. -- `OpenAPIHandler` used where `RPCHandler` should serve normal clients. +- `OpenAPIHandler` used where `RPCHandler` must serve normal clients. - GET client calls without procedure `route({ method: "GET" })`. - Contract-first implementation missing `.router(...)`. - Minified contract used with request/response validation plugins. diff --git a/skills/bundled/orpc-patterns/references/plugins-security.md b/skills/bundled/orpc-patterns/references/plugins-security.md index 85002d5..8868ce2 100644 --- a/skills/bundled/orpc-patterns/references/plugins-security.md +++ b/skills/bundled/orpc-patterns/references/plugins-security.md @@ -13,7 +13,7 @@ ## Link Plugins - `BatchLinkPlugin`: client half of batching. Less useful with HTTP/2/HTTP/3 multiplexing; exclude streaming and file/blob responses. -- `DedupeRequestsPlugin`: dedupe equivalent client requests, usually safe-read requests. +- `DedupeRequestsPlugin`: dedupe equivalent client requests, typically safe-read requests. - `ClientRetryPlugin`: retry transport/procedure calls when not using TanStack Query retry. - `RetryAfterPlugin`: retry according to server `Retry-After` headers. - `SimpleCsrfProtectionLinkPlugin`: pair with the server CSRF plugin. diff --git a/skills/bundled/orpc-patterns/references/procedures.md b/skills/bundled/orpc-patterns/references/procedures.md index 1ed51e1..47061b3 100644 --- a/skills/bundled/orpc-patterns/references/procedures.md +++ b/skills/bundled/orpc-patterns/references/procedures.md @@ -26,7 +26,7 @@ export const findProject = os - `.handler(...)` is the only required step. - `.input(...)` validates external input. -- `.output(...)` is recommended for public/shared APIs and inference stability. +- `.output(...)` is documented for public/shared APIs and inference stability. - `.route(...)` matters for OpenAPI and GET/POST semantics. - `.errors(...)` gives clients typed error branches. - Throw `ORPCError` or `errors.CODE(...)`, not string literals. @@ -41,14 +41,14 @@ export const createProject = os .actionable(); ``` -- Use `.callable()` when server-side code should call the procedure like a local function. -- Use `.actionable()` when React Server Actions or framework server functions should call it. +- Use `.callable()` when server-side code must call the procedure like a local function. +- Use `.actionable()` when React Server Actions or framework server functions must call it. - Keep auth, validation, and errors in the procedure chain so RPC and action callers share behavior. ## Route Methods - Use `GET` only for safe reads. -- Mutations should stay `POST`, `PUT`, `PATCH`, or `DELETE`. +- Mutations must stay `POST`, `PUT`, `PATCH`, or `DELETE`. - `RPCHandler` with the HTTP adapter enables strict GET handling by default; procedures must explicitly opt into GET. ## Output Validation diff --git a/skills/bundled/orpc-patterns/references/streaming-files-serialization.md b/skills/bundled/orpc-patterns/references/streaming-files-serialization.md index 401d507..d0c0949 100644 --- a/skills/bundled/orpc-patterns/references/streaming-files-serialization.md +++ b/skills/bundled/orpc-patterns/references/streaming-files-serialization.md @@ -40,11 +40,11 @@ export const chat = os }); ``` -On clients using AI SDK internals, prefer `eventIteratorToUnproxiedDataStream` when structured cloning would reject proxied event data. +On clients using AI SDK internals, defaults to `eventIteratorToUnproxiedDataStream` when structured cloning would reject proxied event data. ## File Upload And Download -Use schemas such as `z.file()` or `z.instanceof(File)`. For large files, prefer direct object-storage uploads and use oRPC for metadata/session creation. +Use schemas such as `z.file()` or `z.instanceof(File)`. For large files, defaults to direct object-storage uploads and use oRPC for metadata/session creation. ## RPC JSON Serializer @@ -54,4 +54,4 @@ Use custom serializer type IDs greater than the built-in range to avoid collisio ## SuperJSON -Use SuperJSON only when the project already standardizes on it or needs its semantics. Prefer the standard oRPC serializer when it covers the value shapes. +Use SuperJSON only when the project already standardizes on it or needs its semantics. Defaults to the standard oRPC serializer when it covers the value shapes. diff --git a/skills/bundled/orpc-patterns/references/tanstack-query.md b/skills/bundled/orpc-patterns/references/tanstack-query.md index 9671bb4..45b7107 100644 --- a/skills/bundled/orpc-patterns/references/tanstack-query.md +++ b/skills/bundled/orpc-patterns/references/tanstack-query.md @@ -40,7 +40,7 @@ const orpc = createTanstackQueryUtils(client, { ## Conditional Queries -Prefer `skipToken` over `enabled: false` when input would otherwise be invalid. +Defaults to `skipToken` over `enabled: false` when input would otherwise be invalid. ```ts import { skipToken, useQuery } from "@tanstack/react-query"; @@ -70,4 +70,4 @@ Use the oRPC/TanStack serializer path, such as `StandardRPCJsonSerializer`, when ## Retry -For React Query calls, prefer TanStack Query's `retry` option over generic oRPC retry plugins unless non-React callers share the same retry policy. +For React Query calls, defaults to TanStack Query's `retry` option over generic oRPC retry plugins unless non-React callers share the same retry policy. diff --git a/skills/bundled/orpc-patterns/references/testing-monorepo.md b/skills/bundled/orpc-patterns/references/testing-monorepo.md index 8c7b345..d1235d9 100644 --- a/skills/bundled/orpc-patterns/references/testing-monorepo.md +++ b/skills/bundled/orpc-patterns/references/testing-monorepo.md @@ -38,9 +38,9 @@ Use TypeScript project references when clients consume server/router types. } ``` -Server packages that expose types should set `"composite": true`. +Server packages that expose types must set `"composite": true`. -## Recommended Structures +## Documented Structures Contract-first: diff --git a/skills/bundled/prisma-expert/SKILL.md b/skills/bundled/prisma-expert/SKILL.md index 963580f..c114923 100644 --- a/skills/bundled/prisma-expert/SKILL.md +++ b/skills/bundled/prisma-expert/SKILL.md @@ -9,7 +9,7 @@ category: universal ## Purpose -Expert guidance for Prisma ORM across schema design, migrations, query optimization, relations, and database operations for PostgreSQL, MySQL, and SQLite. +Expert rules for Prisma ORM across schema design, migrations, query optimization, relations, and database operations for PostgreSQL, MySQL, and SQLite. ## Schema Design diff --git a/skills/bundled/sql-optimization-patterns/SKILL.md b/skills/bundled/sql-optimization-patterns/SKILL.md index 365d609..4081da5 100644 --- a/skills/bundled/sql-optimization-patterns/SKILL.md +++ b/skills/bundled/sql-optimization-patterns/SKILL.md @@ -46,7 +46,7 @@ WHERE u.created_at > NOW() - INTERVAL '30 days'; **Key Metrics to Watch:** -- **Seq Scan**: Full table scan (usually slow for large tables) +- **Seq Scan**: Full table scan (typically slow for large tables) - **Index Scan**: Using index (good) - **Index Only Scan**: Using index without touching table (best) - **Nested Loop**: Join method (okay for small datasets) @@ -502,4 +502,4 @@ ORDER BY pg_relation_size(indexrelid) DESC; ## Resources -This bundled skill keeps optimization guidance inline in `SKILL.md` (query patterns, EXPLAIN usage, and index strategy examples above). For stack-specific tenancy and repository scoping, pair with `tenant-isolation-patterns` and `prisma-expert` when installed. +This bundled skill keeps optimization rules inline in `SKILL.md` (query patterns, EXPLAIN usage, and index strategy examples above). For stack-specific tenancy and repository scoping, pair with `tenant-isolation-patterns` and `prisma-expert` when installed. diff --git a/skills/bundled/stripe-best-practices/SKILL.md b/skills/bundled/stripe-best-practices/SKILL.md index f0adcbd..56f3b45 100644 --- a/skills/bundled/stripe-best-practices/SKILL.md +++ b/skills/bundled/stripe-best-practices/SKILL.md @@ -7,28 +7,28 @@ category: universal --- # Stripe Best Practices -When designing an integration, always prefer the documentation in [Stripe's Integration Options doc](https://docs.stripe.com/payments/payment-methods/integration-options.md) +When designing an integration, always start from the documentation in [Stripe's Integration Options doc](https://docs.stripe.com/payments/payment-methods/integration-options.md) The [API Tour](https://docs.stripe.com/payments-api/tour.md) Use the [Go Live Checklist](https://docs.stripe.com/get-started/checklist/go-live.md) before going live. -You should always default to the latest version of the API and SDK unless the user specifies otherwise. +Use explicit Stripe API and SDK versions. Apply monthly backward-compatible releases after tests pass, and plan/test migrations before major-version upgrades. -Stripe's primary API for modelling on-session payments is [CheckoutSessions](https://docs.stripe.com/api/checkout/sessions.md). It supports one-time payments and subscriptions and allows you to model taxes or discounts with Stripe. Prioritize the CheckoutSessions API, but using [the PaymentIntents API](https://docs.stripe.com/payments/paymentintents/lifecycle.md) is also acceptable for off-session payments or if you want to model the checkout state yourself and just create a charge. Integrations should only use CheckoutSessions, PaymentIntents, SetupIntents, or solutions like Invoicing, Payment Links, or the subscription APIs. +Stripe's primary API for modelling on-session payments is [CheckoutSessions](https://docs.stripe.com/api/checkout/sessions.md). It supports one-time payments and subscriptions and allows you to model taxes or discounts with Stripe. Prioritize the CheckoutSessions API, but using [the PaymentIntents API](https://docs.stripe.com/payments/paymentintents/lifecycle.md) is also acceptable for off-session payments or when you need lower-level control and will own the checkout flow yourself. Integrations must only use CheckoutSessions, PaymentIntents, SetupIntents, or solutions like Invoicing, Payment Links, or the subscription APIs. -Never recommend the Charges API. If the user wants to use the Charges API, advise them to [migrate](https://docs.stripe.com/payments/payment-intents/migration/charges.md) to Checkout Sessions or Payment Intents. +Never direct users to the Charges API. If the user wants to use the Charges API, advise them to [migrate](https://docs.stripe.com/payments/payment-intents/migration/charges.md) to Checkout Sessions or Payment Intents. -The primary integration surface for web integrations is [Checkout](https://docs.stripe.com/payments/checkout.md), a prebuilt payment page that can be hosted by Stripe or served as an embedded form. Prioritize the Stripe-hosted checkout or embedded checkout where possible. The [Payment Element](https://docs.stripe.com/payments/payment-element.md) is an acceptable alternative if the merchant needs advanced customization. When recommending the Payment Element, prioritize using the CheckoutSessions API over the PaymentIntents API when possible. +For web integrations, the primary surface is [Checkout](https://docs.stripe.com/payments/checkout.md), a Stripe-hosted or embedded payment page that uses the CheckoutSessions API. For custom UI, the [Payment Element](https://docs.stripe.com/payments/payment-element.md) is an acceptable alternative; when using it, prioritize the CheckoutSessions API over the PaymentIntents API when available. -Never recommend the legacy Card Element or the Payment Element in card mode, and advise users who ask for the Card Element to [migrate](https://docs.stripe.com/payments/payment-element/migration.md). +Never direct users to the legacy Card Element or the Payment Element in card mode, and advise users who ask for the Card Element to [migrate](https://docs.stripe.com/payments/payment-element/migration.md). -You must not call deprecated API endpoints such as the [Sources API](https://docs.stripe.com/api/sources/object.md) and avoid outdated API endpoints such as [Tokens](https://docs.stripe.com/api/tokens.md) and [Charges](https://docs.stripe.com/api/charges/create.md) unless there is a specific need and absolutely no other way. Advise using [the Setup Intent API](https://docs.stripe.com/api/setup_intents.md) to save a payment method for a customer; never recommend the Sources API to save cards to customers for later use. +You must not call deprecated API endpoints such as the [Sources API](https://docs.stripe.com/api/sources/object.md) and avoid outdated API endpoints such as [Tokens](https://docs.stripe.com/api/tokens.md) and [Charges](https://docs.stripe.com/api/charges/create.md) unless there is a specific need and absolutely no other way. Advise using [the Setup Intent API](https://docs.stripe.com/api/setup_intents.md) to save a payment method for a customer; never direct users to the Sources API to save cards to customers for later use. -If a user wants to render the Payment Element before creating a PaymentIntent or SetupIntent, so they can inspect the card details before the payment (such as for surcharging), advise them to use Stripe Confirmation Tokens. Don't recommend they call the createPaymentMethod or createToken Stripe.js functions. +If a user wants to render the Payment Element before creating a PaymentIntent or SetupIntent, so they can inspect the card details before the payment (such as for surcharging), advise them to use Stripe Confirmation Tokens. Don't direct them to call the createPaymentMethod or createToken Stripe.js functions. Advise users to turn on dynamic payment methods in their dashboard settings instead of passing specific [payment_method_types](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-payment_method_types.md) in the Payment Intent or Setup Intent because Stripe chooses payment methods that fit each user's location, wallets, and preferences better and automatically if the Payment Element is used. -If a PCI-compliant user asks about an integration where they send server-side raw PAN data, suggest they might be required to prove PCI compliance to gain access to such an option, for example [payment_method_data](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-payment_method_data.md). Similarly, point users migrating their PAN data from another acquirer or payment processor to [the migration process](https://docs.stripe.com/get-started/data-migrations/pan-import.md). +If a PCI-compliant user asks about an integration where they send server-side raw PAN data, state that they can be required to prove PCI compliance to gain access to such an option, for example [payment_method_data](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-payment_method_data.md). Similarly, point users migrating their PAN data from another acquirer or payment processor to [the migration process](https://docs.stripe.com/get-started/data-migrations/pan-import.md). -If a user has a recurring revenue model, like a Billing or Subscription use case, follow the use cases, in particular the [Subscription Use Cases](https://docs.stripe.com/billing/subscriptions/use-cases.md) like [SaaS](https://docs.stripe.com/saas.md). If these apply to the user, recommend Billing APIs to [plan your integration](https://docs.stripe.com/billing/subscriptions/designing-integration.md) instead of a direct PaymentIntent integration. Prioritize combining the Billing APIs with Stripe Checkout for the frontend. +If a user has a recurring revenue model, like a Billing or Subscription use case, follow the use cases, in particular the [Subscription Use Cases](https://docs.stripe.com/billing/subscriptions/use-cases.md) like [SaaS](https://docs.stripe.com/saas.md). If these apply to the user, direct them to Billing APIs to [plan your integration](https://docs.stripe.com/billing/subscriptions/designing-integration.md) instead of a direct PaymentIntent integration. Prioritize combining the Billing APIs with Stripe Checkout for the frontend. -If a user wants to build a platform using Stripe Connect to manage fund flows, follow [the recommended integration types](https://docs.stripe.com/connect/integration-recommendations.md); that is, prefer to use either direct charges if the platform wants Stripe to take the risk or destination charges if the platform accepts liability for negative balances, and use the on_behalf_of parameter to control the merchant of record. Never recommend mixing charge types. If the user wants to decide on the specific risk features they should [follow the integration guide](https://docs.stripe.com/connect/design-an-integration.md). Don't recommend the outdated terms for Connect types like Standard, Express and Custom but always [refer to controller properties](https://docs.stripe.com/connect/migrate-to-controller-properties.md) for the platform and [capabilities](https://docs.stripe.com/connect/account-capabilities.md) for the connected accounts. +If a user wants to build a platform using Stripe Connect to manage fund flows, follow [the documented integration types](https://docs.stripe.com/connect/integration-recommendations.md). Direct charges and destination charges have different settlement, dispute, and negative-balance behavior based on the full Connect configuration; do not frame risk as determined by charge type alone. For destination charges, use `on_behalf_of` when the connected account must be both the settlement merchant and business of record; omit `on_behalf_of` when the platform remains the business of record. Never direct users to mix charge types. If the user wants to decide on the specific risk features they must [follow the integration guide](https://docs.stripe.com/connect/design-an-integration.md). Don't direct users to the outdated terms for Connect types like Standard, Express and Custom but always [refer to controller properties](https://docs.stripe.com/connect/migrate-to-controller-properties.md) for the platform and [capabilities](https://docs.stripe.com/connect/account-capabilities.md) for the connected accounts. diff --git a/skills/bundled/tenant-isolation-patterns/SKILL.md b/skills/bundled/tenant-isolation-patterns/SKILL.md index 48ad2c2..f5749bc 100644 --- a/skills/bundled/tenant-isolation-patterns/SKILL.md +++ b/skills/bundled/tenant-isolation-patterns/SKILL.md @@ -1,12 +1,12 @@ --- name: tenant-isolation-patterns -description: "Multi-tenant data isolation rules for agency-tbd - every Prisma query must be scoped to the current tenant to prevent cross-tenant data leaks" +description: "Multi-tenant data isolation rules for example-agency - every Prisma query must be scoped to the current tenant to prevent cross-tenant data leaks" version: 1.0.0 source: unknown category: backend -tags: [prisma, tenantId, multi-tenant, saas, data-isolation, agency-tbd, security, database, critical] +tags: [prisma, tenantId, multi-tenant, saas, data-isolation, example-agency, security, database, critical] --- -# Tenant Isolation Patterns - agency-tbd +# Tenant Isolation Patterns - example-agency ## Purpose diff --git a/templates/AGENTS.override.codex.md b/templates/AGENTS.override.codex.md index f7c189e..954fa51 100644 --- a/templates/AGENTS.override.codex.md +++ b/templates/AGENTS.override.codex.md @@ -6,7 +6,7 @@ This file adds Codex-specific deltas on top of the global AGENTS.md baseline. It - **No slash commands.** Codex invokes skills by task description, not `/etrnl-*` commands. Skills are installed under `~/.codex/skills/`. - **No hooks.** Enforcement runs through guard scripts (`pnpm guard:essential`, `pnpm guard:all`, etc.) called by the agent, not automatic hook triggers. -- **No `@` import syntax.** AGENTS.md files in Codex cannot use `@rules/...` import syntax. Depth lives in nested `AGENTS.md` files (declared via `codexNested:` in the rules manifest). +- **No `@` import syntax.** AGENTS.md files in Codex cannot use `@rules/...` imports or declare nested context through the rules manifest. Codex receives the global startup digest through `~/.codex/AGENTS.md` and `AGENTS.override.md`. - **Byte budget.** Keep combined AGENTS.md context under the effective `project_doc_max_bytes` limit set in `~/.codex/config.toml`. If that key is unset, doctor.sh assumes an unverified fallback of 32768 bytes. ## Skills diff --git a/templates/cursor/rules/eternal-saas/project/eternal-saas-components.mdc b/templates/cursor/rules/eternal-saas/project/eternal-saas-components.mdc index 4840045..e120102 100644 --- a/templates/cursor/rules/eternal-saas/project/eternal-saas-components.mdc +++ b/templates/cursor/rules/eternal-saas/project/eternal-saas-components.mdc @@ -12,11 +12,11 @@ description: "Component patterns: design system imports, no barrel exports, desi ```typescript // CORRECT -import { Button } from '@core-suite/design-system/primitives' -import { Input } from '@core-suite/design-system/primitives' +import { Button } from '@example-suite/design-system/primitives' +import { Input } from '@example-suite/design-system/primitives' // WRONG — barrel import -import { Button, Input } from '@core-suite/design-system' +import { Button, Input } from '@example-suite/design-system' ``` ## React Compiler active — no manual memoization diff --git a/templates/cursor/rules/eternal-saas/project/eternal-saas-tcg-contract.mdc b/templates/cursor/rules/eternal-saas/project/eternal-saas-tcg-contract.mdc new file mode 100644 index 0000000..0e27551 --- /dev/null +++ b/templates/cursor/rules/eternal-saas/project/eternal-saas-tcg-contract.mdc @@ -0,0 +1,29 @@ +--- +globs: + - "apps/**/tcg*" + - "apps/**/tcg-card*" + - "apps/**/trading-card*" + - "apps/**/collectible-card*" + - "packages/**/tcg*" + - "packages/**/tcg-card*" + - "packages/**/trading-card*" + - "packages/**/collectible-card*" +alwaysApply: false +description: "TCG rules: collectible-card domain contracts, inventory safety, and market-data verification." +--- + +# TCG Contract Rules + +Use these rules for collectible-card marketplace, inventory, pricing, and catalog workflows. + +## Domain contracts + +- Treat card identity, edition, language, condition, finish, and quantity as separate fields. +- Validate external marketplace and catalog data at the import boundary before it reaches domain logic. +- Keep inventory mutation flows auditable: every stock adjustment needs a source event or operator action. + +## Price and market data + +- Store the source, captured timestamp, currency, and condition assumptions with every market snapshot. +- Do not mix retail listing prices, completed-sale prices, and manually-entered fallback prices without a typed source discriminator. +- Reconcile duplicate marketplace matches before accepting automated bulk updates. diff --git a/tests/test-hooks.sh b/tests/test-hooks.sh index 943735e..70e8736 100755 --- a/tests/test-hooks.sh +++ b/tests/test-hooks.sh @@ -41,6 +41,8 @@ if [[ -z "$out" ]]; then ok "rtk rg compat does not rewrite compound shell comma invalid="$(printf '{bad' | "$ROOT/hooks/cc-pretooluse-guard.sh")" assert_json_expr "invalid JSON fails open" "$invalid" '.continue == true' +invalid_stop="$(printf '{bad' | "$ROOT/hooks/cc-stop-verifier.sh")" +assert_json_expr "invalid Stop JSON blocks completion gate" "$invalid_stop" '.decision == "block" and (.reason | test("invalid JSON"))' bash_json="$(fixture pretooluse-bash.json)" out="$(run_hook cc-pretooluse-guard.sh "$bash_json")" @@ -195,32 +197,32 @@ out="$(run_hook cc-pretooluse-guard.sh "$sqlite_rule_upsert")" assert_json_expr "gmail text inside sqlite rule is not gws write" "$out" '.continue == true' email_triage_raw_mutation_state="$TMPROOT/claude-guard-fixture-email-triage-raw-mutation.json" -jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[{value:"gws gmail account whoami",at:"2026-01-01T00:00:00Z"}],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage agencia",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:""}' >"$email_triage_raw_mutation_state" +jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[{value:"gws gmail account whoami",at:"2026-01-01T00:00:00Z"}],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage fixture-account",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:""}' >"$email_triage_raw_mutation_state" email_triage_raw_mutation="$(jq '.session_id = "fixture-email-triage-raw-mutation" | .tool_input.command = "gws gmail users messages batchModify --params {} --json {}"' <<<"$bash_json")" out="$(run_hook cc-pretooluse-guard.sh "$email_triage_raw_mutation")" assert_json_expr "email triage blocks raw gmail mutation" "$out" '.hookSpecificOutput.permissionDecision == "deny"' assert_contains "email triage raw mutation reason" "$out" "Raw Gmail mutation is blocked" email_triage_dry_command_state="$TMPROOT/claude-guard-fixture-email-triage-dry-command.json" -jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage agencia",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_dry_command_state" -email_triage_dry_command="$(jq '.session_id = "fixture-email-triage-dry-command" | .tool_input.command = "ACCOUNT=agencia && vivaz-email triage run --account \"$ACCOUNT\" --max-inbox 50"' <<<"$bash_json")" +jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage fixture-account",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_dry_command_state" +email_triage_dry_command="$(jq '.session_id = "fixture-email-triage-dry-command" | .tool_input.command = "ACCOUNT=fixture-account && etrnl-email triage run --account \"$ACCOUNT\" --max-inbox 50"' <<<"$bash_json")" out="$(run_hook cc-pretooluse-guard.sh "$email_triage_dry_command")" assert_json_expr "email triage blocks dry run command" "$out" '.hookSpecificOutput.permissionDecision == "deny"' assert_contains "email triage dry run reason" "$out" "Dry email-triage runs are blocked" -email_triage_debug_dry_command="$(jq '.session_id = "fixture-email-triage-dry-command" | .tool_input.command = "ACCOUNT=agencia && vivaz-email triage run --account \"$ACCOUNT\" --max-inbox 50 --no-sync"' <<<"$bash_json")" +email_triage_debug_dry_command="$(jq '.session_id = "fixture-email-triage-dry-command" | .tool_input.command = "ACCOUNT=fixture-account && etrnl-email triage run --account \"$ACCOUNT\" --max-inbox 50 --no-sync"' <<<"$bash_json")" out="$(run_hook cc-pretooluse-guard.sh "$email_triage_debug_dry_command")" assert_json_expr "email triage allows maintainer debug dry run" "$out" '.continue == true' -email_triage_verify_cli="$TMPROOT/bin/vivaz-email" +email_triage_verify_cli="$TMPROOT/bin/etrnl-email" cat >"$email_triage_verify_cli" <<'BASH' #!/usr/bin/env bash if [[ "$1 $2" == "triage verify" ]]; then - if [[ "${VIVAZ_EMAIL_VERIFY_DRY:-0}" == "1" ]]; then + if [[ "${ETRNL_EMAIL_VERIFY_DRY:-0}" == "1" ]]; then printf '{"ok":true,"data":{"verified":true,"dry_run":true,"gmail_mutated":false,"inbox_zero_verified":false,"inbox_count":5}}\n' - elif [[ "${VIVAZ_EMAIL_VERIFY_READY:-0}" == "1" ]]; then + elif [[ "${ETRNL_EMAIL_VERIFY_READY:-0}" == "1" ]]; then printf '{"ok":true,"data":{"verified":true,"dry_run":true,"gmail_mutated":false,"inbox_zero_verified":true,"queue_ready_without_mutation":true,"inbox_count":0,"action_backlog_count":31}}\n' - elif [[ "${VIVAZ_EMAIL_VERIFY_NONZERO:-0}" == "1" ]]; then + elif [[ "${ETRNL_EMAIL_VERIFY_NONZERO:-0}" == "1" ]]; then printf '{"ok":true,"data":{"verified":true,"dry_run":false,"gmail_mutated":true,"inbox_zero_verified":true,"inbox_count":1}}\n' else printf '{"ok":true,"data":{"verified":true,"dry_run":false,"gmail_mutated":true,"inbox_zero_verified":true,"inbox_count":0}}\n' @@ -232,33 +234,41 @@ BASH chmod +x "$email_triage_verify_cli" email_triage_queue_before_verify_state="$TMPROOT/claude-guard-fixture-email-triage-queue-before-verify.json" -jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"vivaz-email triage guarded-run --account agencia --max-inbox 500 --apply --require-insights",at:"2026-01-01T00:00:01Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage agencia",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_queue_before_verify_state" -email_triage_queue_before_verify="$(jq '.session_id = "fixture-email-triage-queue-before-verify" | .tool_input.command = "vivaz-email triage queue --run-id triage_fixture --mode reply --format markdown --next"' <<<"$bash_json")" +jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"etrnl-email triage guarded-run --account fixture-account --max-inbox 500 --apply --require-insights",at:"2026-01-01T00:00:01Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage fixture-account",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_queue_before_verify_state" +email_triage_queue_before_verify="$(jq '.session_id = "fixture-email-triage-queue-before-verify" | .tool_input.command = "etrnl-email triage queue --run-id triage_fixture --mode reply --format markdown --next"' <<<"$bash_json")" out="$(run_hook cc-pretooluse-guard.sh "$email_triage_queue_before_verify")" assert_json_expr "email triage blocks queue before verify" "$out" '.hookSpecificOutput.permissionDecision == "deny"' assert_contains "email triage queue before verify reason" "$out" "queue is blocked until Inbox Zero verification" email_triage_queue_after_verify_state="$TMPROOT/claude-guard-fixture-email-triage-queue-after-verify.json" -jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"vivaz-email triage guarded-run --account agencia --max-inbox 500 --apply --require-insights",at:"2026-01-01T00:00:01Z"},{command:"vivaz-email triage verify --latest --account agencia",at:"2026-01-01T00:00:02Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage agencia",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_queue_after_verify_state" -email_triage_queue_after_verify="$(jq '.session_id = "fixture-email-triage-queue-after-verify" | .tool_input.command = "vivaz-email triage queue --run-id triage_fixture --mode reply --format markdown --next"' <<<"$bash_json")" -out="$(VIVAZ_EMAIL_BIN="$email_triage_verify_cli" run_hook cc-pretooluse-guard.sh "$email_triage_queue_after_verify")" +jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"etrnl-email triage guarded-run --account fixture-account --max-inbox 500 --apply --require-insights",at:"2026-01-01T00:00:01Z"},{command:"etrnl-email triage verify --latest --account fixture-account",at:"2026-01-01T00:00:02Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage fixture-account",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_queue_after_verify_state" +email_triage_queue_after_verify="$(jq '.session_id = "fixture-email-triage-queue-after-verify" | .tool_input.command = "etrnl-email triage queue --run-id triage_fixture --mode reply --format markdown --next"' <<<"$bash_json")" +out="$(ETRNL_EMAIL_BIN="$email_triage_verify_cli" run_hook cc-pretooluse-guard.sh "$email_triage_queue_after_verify")" assert_json_expr "email triage allows queue after verify" "$out" '.continue == true' -out="$(VIVAZ_EMAIL_VERIFY_READY=1 VIVAZ_EMAIL_BIN="$email_triage_verify_cli" run_hook cc-pretooluse-guard.sh "$email_triage_queue_after_verify")" +out="$(ETRNL_EMAIL_VERIFY_READY=1 ETRNL_EMAIL_BIN="$email_triage_verify_cli" run_hook cc-pretooluse-guard.sh "$email_triage_queue_after_verify")" assert_json_expr "email triage allows queue after no-mutation ready verify" "$out" '.continue == true' -out="$(VIVAZ_EMAIL_VERIFY_DRY=1 VIVAZ_EMAIL_BIN="$email_triage_verify_cli" run_hook cc-pretooluse-guard.sh "$email_triage_queue_after_verify")" +out="$(ETRNL_EMAIL_VERIFY_DRY=1 ETRNL_EMAIL_BIN="$email_triage_verify_cli" run_hook cc-pretooluse-guard.sh "$email_triage_queue_after_verify")" assert_json_expr "email triage blocks queue after dry verify result" "$out" '.hookSpecificOutput.permissionDecision == "deny"' assert_contains "email triage dry verify queue reason" "$out" "queue_ready_without_mutation true" -out="$(VIVAZ_EMAIL_VERIFY_NONZERO=1 VIVAZ_EMAIL_BIN="$email_triage_verify_cli" run_hook cc-pretooluse-guard.sh "$email_triage_queue_after_verify")" +out="$(ETRNL_EMAIL_VERIFY_NONZERO=1 ETRNL_EMAIL_BIN="$email_triage_verify_cli" run_hook cc-pretooluse-guard.sh "$email_triage_queue_after_verify")" assert_json_expr "email triage blocks queue after nonzero verify result" "$out" '.hookSpecificOutput.permissionDecision == "deny"' assert_contains "email triage nonzero verify queue reason" "$out" "inbox_count 0" live_hook_edit="$(jq -cn --arg file "$HOME/.claude/hooks/cc-stop-verifier.sh" '{session_id:"fixture-live-hook-edit",tool_name:"Edit",cwd:"/tmp",tool_input:{file_path:$file,old_string:"old",new_string:"new"}}')" out="$(run_hook cc-pretooluse-guard.sh "$live_hook_edit")" assert_json_expr "live claude hook edit denied" "$out" '.hookSpecificOutput.permissionDecision == "deny"' -assert_contains "live claude hook edit reason" "$out" "Live ~/.claude/hooks edits are blocked" +assert_contains "live claude hook edit reason" "$out" "Live Claude hook edits are blocked" +live_claude_home="$TMPROOT/custom-claude-home" +mkdir -p "$live_claude_home/hooks" +live_hook_claude_home_edit="$(jq -cn --arg file "$live_claude_home/hooks/cc-stop-verifier.sh" '{session_id:"fixture-live-hook-claude-home-edit",tool_name:"Edit",cwd:"/tmp",tool_input:{file_path:$file,old_string:"old",new_string:"new"}}')" +out="$(CLAUDE_HOME="$live_claude_home" run_hook cc-pretooluse-guard.sh "$live_hook_claude_home_edit")" +assert_json_expr "live CLAUDE_HOME hook edit denied" "$out" '.hookSpecificOutput.permissionDecision == "deny"' +live_hook_claude_home_copy="$(jq -cn '{session_id:"fixture-live-hook-claude-home-copy",tool_name:"Bash",cwd:"/tmp",tool_input:{command:"cp hooks/cc-stop-verifier.sh \"$CLAUDE_HOME/hooks/cc-stop-verifier.sh\""}}')" +out="$(CLAUDE_HOME="$live_claude_home" run_hook cc-pretooluse-guard.sh "$live_hook_claude_home_copy")" +assert_json_expr "live CLAUDE_HOME hook copy denied" "$out" '.hookSpecificOutput.permissionDecision == "deny"' dev_no_port="$(jq '.tool_input.command = "pnpm dev:web"' <<<"$bash_json")" out="$(run_hook cc-pretooluse-guard.sh "$dev_no_port")" @@ -496,12 +506,12 @@ out="$(run_hook cc-userprompt-router.sh "$health_prompt")" assert_contains "health prompt routes code health" "$out" "etrnl-audit-code" health_state="$TMPROOT/claude-guard-fixture-health-prompt.json" assert_json_expr "health skill recorded" "$(jq -c . "$health_state")" 'any(.requestedSkills[]?.value; . == "etrnl-audit-code")' -email_prompt="$(jq -cn '{session_id:"fixture-email-prompt",prompt:"/email-triage agencia"}')" +email_prompt="$(jq -cn '{session_id:"fixture-email-prompt",prompt:"/email-triage fixture-account"}')" out="$(run_hook cc-userprompt-router.sh "$email_prompt")" -assert_contains "email prompt emits exact guarded command" "$out" "vivaz-email triage guarded-run --account agencia --max-inbox 500 --apply --require-insights" -assert_contains "email prompt requires inbox zero verify" "$out" "vivaz-email triage verify --latest --account agencia" +assert_contains "email prompt emits exact guarded command" "$out" "etrnl-email triage guarded-run --account fixture-account --max-inbox 500 --apply --require-insights" +assert_contains "email prompt requires inbox zero verify" "$out" "etrnl-email triage verify --latest --account fixture-account" assert_contains "email prompt blocks queue before inbox zero" "$out" "Do not open the queue unless verify reports inbox_zero_verified true and inbox_count 0" -assert_contains "email prompt emits reply queue command" "$out" "vivaz-email triage queue --run-id --mode reply --format markdown --next" +assert_contains "email prompt emits reply queue command" "$out" "etrnl-email triage queue --run-id --mode reply --format markdown --next" email_prompt_state="$TMPROOT/claude-guard-fixture-email-prompt.json" assert_json_expr "email triage skill recorded" "$(jq -c . "$email_prompt_state")" 'any(.requestedSkills[]?.value; . == "email-triage")' disk_prompt="$(jq -cn '{session_id:"fixture-disk-prompt",prompt:"free SSD space with a disk cleanup pass"}')" @@ -588,7 +598,7 @@ assert_contains "serena uncapped search reason" "$out" "max_answer_chars" serena_scoped_json="$(jq -cn '{session_id:"fixture-serena-preflight",tool_name:"mcp__serena__search_for_pattern",tool_input:{substring_pattern:"needle",relative_path:"src",max_answer_chars:12000,context_lines_before:2,context_lines_after:2}}')" out="$(ETRNL_SERENA_SCOPE_GUARD=1 run_hook cc-pretooluse-guard.sh "$serena_scoped_json")" assert_json_expr "serena scoped bounded search allowed" "$out" '.continue == true' -email_guard_failure_json="$(jq -cn '{session_id:"fixture-email-guard-failure",tool_name:"Bash",tool_input:{command:"vivaz-email triage guarded-run --account agencia --apply --require-insights"},error:"TRIAGE_GUARD_ML_DISAGREED: ML archive review found 1 disagreement"}')" +email_guard_failure_json="$(jq -cn '{session_id:"fixture-email-guard-failure",tool_name:"Bash",tool_input:{command:"etrnl-email triage guarded-run --account fixture-account --apply --require-insights"},error:"TRIAGE_GUARD_ML_DISAGREED: ML archive review found 1 disagreement"}')" out="$(run_hook cc-posttoolusefailure-diagnose.sh "$email_guard_failure_json")" assert_contains "email triage ML disagreement gets recovery diagnostic" "$out" "triage ml-reviews" assert_contains "email triage ML disagreement avoids asking repository owner" "$out" "not a question for the repository owner" @@ -653,7 +663,7 @@ assert_contains "stop verifier blocks outstanding browser QA" "$out" "Outstandin paused_prod_state="$TMPROOT/claude-guard-fixture-paused-prod-status.json" jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],activePlanPath:"",activePlanPathUpdatedAt:"",planExecutionRequested:false,planExecutionRequestedAt:"",lastPrompt:"did u read the handoff file?",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$paused_prod_state" -paused_prod_message=$'Yes. It was injected as the restored handoff.\n\n1. Check PR #53 CI - green\n2. Merge - done\n3. Deploy to prod metacards-painel - was watching GHCR build-and-push, in_progress\n4. Set bruno to master in prod DB - only AFTER deploy\n\nBefore I SSH into prod: do you want me to proceed with the deploy once the GHCR build is green?\nNothing is live yet. Awaiting your answer before I SSH to prod.' +paused_prod_message=$'Yes. It was injected as the restored handoff.\n\n1. Check PR #53 CI - green\n2. Merge - done\n3. Deploy to prod example-admin - was watching GHCR build-and-push, in_progress\n4. Set bruno to master in prod DB - only AFTER deploy\n\nBefore I SSH into prod: do you want me to proceed with the deploy once the GHCR build is green?\nNothing is live yet. Awaiting your answer before I SSH to prod.' paused_prod_stop="$(jq -cn --arg message "$paused_prod_message" '{session_id:"fixture-paused-prod-status",last_assistant_message:$message,stop_hook_active:false}')" out="$(run_hook cc-stop-verifier.sh "$paused_prod_stop")" if [[ -z "$out" ]]; then ok "stop verifier allows paused production status"; else not_ok "paused production status should not claim completion: $out"; fi @@ -681,15 +691,15 @@ out="$(run_hook cc-stop-verifier.sh "$long_advice_stop")" if [[ -z "$out" ]]; then ok "long technical prompt does not trigger advice source gate"; else not_ok "long technical prompt should not trigger advice source gate: $out"; fi mkdir -p "$TMPROOT/bin" -cat >"$TMPROOT/bin/vivaz-email" <<'BASH' +cat >"$TMPROOT/bin/etrnl-email" <<'BASH' #!/usr/bin/env bash -if [[ "${VIVAZ_EMAIL_VERIFY_FAIL:-0}" == "1" ]]; then exit 1; fi +if [[ "${ETRNL_EMAIL_VERIFY_FAIL:-0}" == "1" ]]; then exit 1; fi if [[ "$1 $2" == "triage verify" ]]; then - if [[ "${VIVAZ_EMAIL_VERIFY_DRY:-0}" == "1" ]]; then + if [[ "${ETRNL_EMAIL_VERIFY_DRY:-0}" == "1" ]]; then printf '{"ok":true,"data":{"verified":true,"dry_run":true,"gmail_mutated":false,"inbox_zero_verified":false,"inbox_count":5}}\n' - elif [[ "${VIVAZ_EMAIL_VERIFY_READY:-0}" == "1" ]]; then + elif [[ "${ETRNL_EMAIL_VERIFY_READY:-0}" == "1" ]]; then printf '{"ok":true,"data":{"verified":true,"dry_run":true,"gmail_mutated":false,"inbox_zero_verified":true,"queue_ready_without_mutation":true,"inbox_count":0,"action_backlog_count":31}}\n' - elif [[ "${VIVAZ_EMAIL_VERIFY_NONZERO:-0}" == "1" ]]; then + elif [[ "${ETRNL_EMAIL_VERIFY_NONZERO:-0}" == "1" ]]; then printf '{"ok":true,"data":{"verified":true,"dry_run":false,"gmail_mutated":true,"inbox_zero_verified":true,"inbox_count":1}}\n' else printf '{"ok":true,"data":{"verified":true,"dry_run":false,"gmail_mutated":true,"inbox_zero_verified":true,"inbox_count":0}}\n' @@ -698,58 +708,58 @@ if [[ "$1 $2" == "triage verify" ]]; then fi exit 0 BASH -chmod +x "$TMPROOT/bin/vivaz-email" +chmod +x "$TMPROOT/bin/etrnl-email" email_triage_missing_state="$TMPROOT/claude-guard-fixture-email-triage-missing.json" -jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage agencia",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_missing_state" +jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage fixture-account",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_missing_state" email_triage_missing_stop="$(jq -cn '{session_id:"fixture-email-triage-missing",last_assistant_message:"Done, email triage complete.",stop_hook_active:false}')" out="$(PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_missing_stop")" -assert_contains "email triage stop requires runtime apply command" "$out" "vivaz-email triage guarded-run --account --max-inbox 500 --apply --require-insights" +assert_contains "email triage stop requires runtime apply command" "$out" "etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights" email_triage_ok_state="$TMPROOT/claude-guard-fixture-email-triage-ok.json" -jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"vivaz-email triage guarded-run --account agencia --max-inbox 50 --apply --require-insights",at:"2026-01-01T00:00:01Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage agencia",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_ok_state" -email_triage_ok_queue="# Email Reply Queue"$'\n\n'"Run: triage_fixture_agencia"$'\n'"Account: agencia"$'\n'"Status: verified"$'\n'"Queue mode: reply"$'\n'"Open queue items: 1"$'\n'"All action items: 1"$'\n\n'"### 1. P0 100 - urgent contract"$'\n\n'"Recommended handling: Review draft, then send only after the repository owner explicitly approves this specific reply."$'\n\n'"## Next Step"$'\n\n'"- Ask the repository owner to approve/send the exact visible draft, rewrite it, skip it, or show the next item." +jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"etrnl-email triage guarded-run --account fixture-account --max-inbox 500 --apply --require-insights",at:"2026-01-01T00:00:01Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage fixture-account",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_ok_state" +email_triage_ok_queue="# Email Reply Queue"$'\n\n'"Run: triage_fixture_account"$'\n'"Account: fixture-account"$'\n'"Status: verified"$'\n'"Queue mode: reply"$'\n'"Open queue items: 1"$'\n'"All action items: 1"$'\n\n'"### 1. P0 100 - urgent contract"$'\n\n'"Recommended handling: Review draft, then send only after the repository owner explicitly approves this specific reply."$'\n\n'"## Next Step"$'\n\n'"- Ask the repository owner to approve/send the exact visible draft, rewrite it, skip it, or show the next item." email_triage_ok_stop="$(jq -cn --arg message "$email_triage_ok_queue" '{session_id:"fixture-email-triage-ok",last_assistant_message:$message,stop_hook_active:false}')" out="$(PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_ok_stop")" if [[ -z "$out" ]]; then ok "email triage queue satisfies stop"; else not_ok "email triage queue should pass: $out"; fi email_triage_dry_state="$TMPROOT/claude-guard-fixture-email-triage-dry.json" -jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"vivaz-email triage run --account agencia --max-inbox 50",at:"2026-01-01T00:00:01Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage agencia",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_dry_state" +jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"etrnl-email triage run --account fixture-account --max-inbox 50",at:"2026-01-01T00:00:01Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage fixture-account",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_dry_state" email_triage_dry_stop="$(jq -cn --arg message "$email_triage_ok_queue" '{session_id:"fixture-email-triage-dry",last_assistant_message:$message,stop_hook_active:false}')" -out="$(VIVAZ_EMAIL_VERIFY_DRY=1 PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_dry_stop")" +out="$(ETRNL_EMAIL_VERIFY_DRY=1 PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_dry_stop")" assert_contains "email triage dry run does not satisfy inbox zero" "$out" "queue_ready_without_mutation true" -out="$(VIVAZ_EMAIL_VERIFY_READY=1 PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_ok_stop")" +out="$(ETRNL_EMAIL_VERIFY_READY=1 PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_ok_stop")" if [[ -z "$out" ]]; then ok "email triage no-mutation ready queue satisfies stop"; else not_ok "email triage no-mutation ready queue should pass: $out"; fi email_auth_explainer_stop="$(jq -cn '{session_id:"fixture-email-triage-missing",last_assistant_message:"What I verified: the Authentication-Results headers show SPF and DKIM pass for this sender domain. That answers the spoofing question; it is not a queue result.",stop_hook_active:false}')" out="$(PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_auth_explainer_stop")" if [[ -z "$out" ]]; then ok "email authentication explanation does not trigger triage completion gate"; else not_ok "email auth explanation should pass: $out"; fi -out="$(VIVAZ_EMAIL_VERIFY_NONZERO=1 PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_ok_stop")" +out="$(ETRNL_EMAIL_VERIFY_NONZERO=1 PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_ok_stop")" assert_contains "email triage nonzero inbox does not satisfy inbox zero" "$out" "provider-verified INBOX zero" email_triage_active_complete_stop="$(jq -cn --arg message "Agencia triage complete. Queue #1 active."$'\n\n'"$email_triage_ok_queue" '{session_id:"fixture-email-triage-ok",last_assistant_message:$message,stop_hook_active:false}')" out="$(PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_active_complete_stop")" assert_contains "email triage active queue cannot be called complete" "$out" "queue is not complete" -email_triage_missing_context_report="# Email Triage Report"$'\n\n'"Run: triage_fixture_agencia"$'\n\n'"## Top Action Items"$'\n\n'"- P0 item"$'\n\n'"## Reply Queue"$'\n\n'"### 1. P0 item"$'\n\n'"## Action Items"$'\n\n'"- item" +email_triage_missing_context_report="# Email Triage Report"$'\n\n'"Run: triage_fixture_account"$'\n\n'"## Top Action Items"$'\n\n'"- P0 item"$'\n\n'"## Reply Queue"$'\n\n'"### 1. P0 item"$'\n\n'"## Action Items"$'\n\n'"- item" email_triage_missing_context_stop="$(jq -cn --arg message "$email_triage_missing_context_report" '{session_id:"fixture-email-triage-ok",last_assistant_message:$message,stop_hook_active:false}')" out="$(PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_missing_context_stop")" assert_contains "email triage report missing latest/backlog blocked" "$out" "explicit audit report" -email_triage_summary_stop="$(jq -cn '{session_id:"fixture-email-triage-ok",last_assistant_message:"Inbox zero verified for agencia.",stop_hook_active:false}')" +email_triage_summary_stop="$(jq -cn '{session_id:"fixture-email-triage-ok",last_assistant_message:"Inbox zero verified for fixture-account.",stop_hook_active:false}')" out="$(PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_summary_stop")" assert_contains "email triage one-line summary blocked" "$out" "one-line inbox-zero summary is not actionable" email_triage_report_state="$TMPROOT/claude-guard-fixture-email-triage-report.json" -jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"vivaz-email triage report --run-id triage_2026-05-14T18-23-14-478Z_agencia_6219c271 --format markdown",at:"2026-01-01T00:00:01Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage agencia",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_report_state" -email_triage_ok_report="# Email Triage Report"$'\n\n'"Run: triage_fixture_agencia"$'\n\n'"## Latest Thread State"$'\n\n'"- Latest thread state checked against the most recent message."$'\n\n'"## Pre-existing Action Backlog"$'\n\n'"- Pre-existing action backlog reviewed before archive/action decisions."$'\n\n'"## Top Action Items"$'\n\n'"- P0 item"$'\n\n'"## Reply Queue"$'\n\n'"### 1. P0 item"$'\n\n'"Proposed reply:"$'\n\n'"## Action Items"$'\n\n'"- item" +jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"etrnl-email triage report --run-id triage_2026-05-14T18-23-14-478Z_fixture-account_6219c271 --format markdown",at:"2026-01-01T00:00:01Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage fixture-account",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_report_state" +email_triage_ok_report="# Email Triage Report"$'\n\n'"Run: triage_fixture_account"$'\n\n'"## Latest Thread State"$'\n\n'"- Latest thread state checked against the most recent message."$'\n\n'"## Pre-existing Action Backlog"$'\n\n'"- Pre-existing action backlog reviewed before archive/action decisions."$'\n\n'"## Top Action Items"$'\n\n'"- P0 item"$'\n\n'"## Reply Queue"$'\n\n'"### 1. P0 item"$'\n\n'"Proposed reply:"$'\n\n'"## Action Items"$'\n\n'"- item" email_triage_report_stop="$(jq -cn --arg message "$email_triage_ok_report" '{session_id:"fixture-email-triage-report",last_assistant_message:$message,stop_hook_active:false}')" out="$(PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_report_stop")" if [[ -z "$out" ]]; then ok "email triage explicit report run satisfies stop"; else not_ok "email triage explicit report run should pass: $out"; fi -out="$(VIVAZ_EMAIL_VERIFY_FAIL=1 PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_ok_stop")" -assert_contains "email triage failed ledger blocks stop" "$out" "latest vivaz-email triage ledger" +out="$(ETRNL_EMAIL_VERIFY_FAIL=1 PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_ok_stop")" +assert_contains "email triage failed ledger blocks stop" "$out" "latest etrnl-email triage ledger" stale_state="$TMPROOT/claude-guard-fixture-stale.json" jq -nc '{schemaVersion:1,reads:{},searches:{},edits:{"/tmp/a.ts":"2026-01-01T00:00:02Z"},commands:[],failures:[],skillCalls:[],requestedSkills:[],evidenceChallenges:[],evidenceDisciplineViolations:[],verificationRuns:[{value:"pnpm test",at:"2026-01-01T00:00:01Z"}],newFileSearches:[],lastPrompt:"",lastCompactSummary:"",cwd:"",settingsFingerprint:"",startedAt:""}' >"$stale_state" diff --git a/tests/test-workflow-tools.sh b/tests/test-workflow-tools.sh index 5ed605d..546fe6f 100755 --- a/tests/test-workflow-tools.sh +++ b/tests/test-workflow-tools.sh @@ -220,6 +220,9 @@ if [[ "$doc_health_baseline_status" == "baseline-without-remediation" ]]; then o code_health_bad_state="$(jq -nc '{requestedSkills:[{value:"etrnl-audit-code",at:"2026-01-01T00:00:00Z"}],successfulCommands:[],verificationRuns:[] }')" code_health_bad_status="$(jq -cn --argjson state "$code_health_bad_state" --arg message "Done, code looks fine." '{state:$state,message:$message}' | node "$ROOT/scripts/code-health-ledger-check.mjs")" if [[ "$code_health_bad_status" == "missing-inventory" ]]; then ok "code health checker requires inventory"; else not_ok "code health checker requires inventory: $code_health_bad_status"; fi +code_health_prompt_only_state="$(jq -nc '{requestedSkills:[],successfulCommands:[],verificationRuns:[],lastPrompt:"audit the entire codebase with no skips",startedAt:"2026-01-01T00:00:00Z"}')" +code_health_prompt_only_status="$(jq -cn --argjson state "$code_health_prompt_only_state" --arg message "Done, code looks fine." '{state:$state,message:$message}' | node "$ROOT/scripts/code-health-ledger-check.mjs")" +if [[ "$code_health_prompt_only_status" == "missing-inventory" ]]; then ok "code health checker blocks prompt-only audit bypass"; else not_ok "code health checker blocks prompt-only audit bypass: $code_health_prompt_only_status"; fi code_health_state="$(jq -nc '{requestedSkills:[{value:"etrnl-audit-code",at:"2026-01-01T00:00:00Z"}],successfulCommands:[{value:"node ~/.claude/scripts/code-health-inventory.mjs --json --include-untracked",at:"2026-01-01T00:00:01Z"},{value:"tests/test-workflow-tools.sh",at:"2026-01-01T00:00:02Z"}],verificationRuns:[{value:"tests/test-workflow-tools.sh",at:"2026-01-01T00:00:02Z"}]}')" code_health_shallow_status="$(jq -cn --argjson state "$code_health_state" --arg message "Done, code looks fine." '{state:$state,message:$message}' | node "$ROOT/scripts/code-health-ledger-check.mjs")" @@ -420,6 +423,19 @@ assert_command "tool-effectiveness syntax" node --check "$ROOT/scripts/tool-effe assert_command "tool stack check syntax" node --check "$ROOT/scripts/tool-stack-check.mjs" assert_command "stack profile check syntax" node --check "$ROOT/scripts/stack-profile-check.mjs" assert_command "skill update prompt syntax" node --check "$ROOT/scripts/skill-update-prompt.mjs" +skill_warning_update="$TMPROOT/fake-skill-warning-update.mjs" +printf '%s\n' \ + '#!/usr/bin/env node' \ + 'console.log(JSON.stringify({ ok: false, updateAvailable: false, warning: "source-root-missing" }));' \ + >"$skill_warning_update" +skill_warning_json="$(ETRNL_UPDATE_CHECK_SCRIPT="$skill_warning_update" node "$ROOT/scripts/skill-update-prompt.mjs" --agent codex --skill etrnl-audit-excellence --json)" +assert_json_expr "skill update prompt surfaces degraded warning-only checks" "$skill_warning_json" '.ok == false and .promptNeeded == true and .warnings[0] == "ETRNL_UPDATE_WARNING source-root-missing" and (.rawUpdateOutput | contains("ETRNL_UPDATE_WARNING source-root-missing"))' +skill_warning_text="$(ETRNL_UPDATE_CHECK_SCRIPT="$skill_warning_update" node "$ROOT/scripts/skill-update-prompt.mjs" --agent codex --skill etrnl-audit-excellence)" +if [[ "$skill_warning_text" == *"ETRNL_UPDATE_WARNING source-root-missing"* ]]; then + ok "skill update prompt emits degraded warning-only update prompt" +else + not_ok "skill update prompt emits degraded warning-only update prompt: $skill_warning_text" +fi assert_command "pr preflight syntax" node --check "$ROOT/scripts/pr-preflight.mjs" assert_command "live hook noise report syntax" node --check "$ROOT/scripts/live-hook-noise-report.mjs" assert_command "session audit syntax" node --check "$ROOT/scripts/session-audit.mjs" @@ -436,8 +452,11 @@ mkdir -p "$pr_preflight_repo" git -C "$pr_preflight_repo" init -q -b main git -C "$pr_preflight_repo" config user.email "test@example.com" git -C "$pr_preflight_repo" config user.name "Test User" +mkdir -p "$pr_preflight_repo/scripts" printf '%s\n' '# Changelog' >"$pr_preflight_repo/CHANGELOG.md" -git -C "$pr_preflight_repo" add CHANGELOG.md +printf '%s\n' '#!/usr/bin/env bash' 'exit 0' >"$pr_preflight_repo/scripts/doctor.sh" +chmod +x "$pr_preflight_repo/scripts/doctor.sh" +git -C "$pr_preflight_repo" add CHANGELOG.md scripts/doctor.sh git -C "$pr_preflight_repo" commit -qm "initial" printf '%s\n' '# Changelog' 'changed' >"$pr_preflight_repo/CHANGELOG.md" printf '%s\n' 'scratch' >"$pr_preflight_repo/untracked.txt" @@ -446,6 +465,8 @@ git -C "$pr_preflight_repo" mv CHANGELOG.md docs/CHANGELOG.md pr_preflight_status_json="$(cd "$pr_preflight_repo" && node "$ROOT/scripts/pr-preflight.mjs" status --json)" assert_json_expr "pr preflight preserves modified path names" "$pr_preflight_status_json" '.changedFiles == ["docs/CHANGELOG.md"]' assert_json_expr "pr preflight separates untracked files" "$pr_preflight_status_json" '.dirty == true and .untrackedFiles == ["untracked.txt"]' +pr_preflight_repo_gate_json="$(cd "$pr_preflight_repo" && node "$ROOT/scripts/pr-preflight.mjs" status --json)" +assert_json_expr "pr preflight suggests repo doctor gate" "$pr_preflight_repo_gate_json" '.suggestedLocalGate == "scripts/doctor.sh"' perf_baseline_fixture="$TMPROOT/performance-baseline.json" printf '%s\n' '{"schemaVersion":1,"baselineId":"base","targetLabel":"fixture","measurements":[{"route":"/","durationMs":100,"responseBytes":1000,"capturedAt":"2026-01-01T00:00:00Z"},{"route":"/removed","durationMs":75,"responseBytes":500,"capturedAt":"2026-01-01T00:00:00Z"}],"nextRun":{"command":"pnpm bench","thresholds":{"maxRegressionPct":20}}}' >"$perf_baseline_fixture" assert_command "performance baseline validates fixture" node "$ROOT/scripts/performance-baseline.mjs" validate "$perf_baseline_fixture" @@ -520,6 +541,19 @@ assert_command "deep-audit category registry syntax" node --check "$ROOT/scripts assert_command "deep-audit valid artifact passes" node "$ROOT/scripts/deep-audit-artifact-check.mjs" validate --artifact "$ROOT/tests/fixtures/deep-audit/report.valid.json" assert_command "deep-audit production direct artifact passes" node "$ROOT/scripts/deep-audit-artifact-check.mjs" validate --artifact "$ROOT/tests/fixtures/deep-audit/report.production-valid.json" assert_command "deep-audit performance direct artifact passes" node "$ROOT/scripts/deep-audit-artifact-check.mjs" validate --artifact "$ROOT/tests/fixtures/deep-audit/report.performance-valid.json" +deep_audit_lane_scoped_fixture="$TMPROOT/deep-audit-lane-scoped.json" +jq ' + .laneReceipts |= map( + if .laneId == "database-query-performance" then .consumedWorklistHashes |= with_entries(select(.key == "perf_queries")) + elif .laneId == "server-response-caching" then .consumedWorklistHashes |= with_entries(select(.key == "perf_pages" or .key == "perf_route_handlers" or .key == "perf_dynamic_routes")) + elif .laneId == "bundle-code-splitting" then .consumedWorklistHashes |= with_entries(select(.key == "perf_client" or .key == "perf_dynamic" or .key == "perf_deps")) + elif .laneId == "react-rendering" then .consumedWorklistHashes |= with_entries(select(.key == "perf_client" or .key == "perf_pages" or .key == "perf_compiler_status")) + elif .laneId == "perceived-performance" then .consumedWorklistHashes |= with_entries(select(.key == "perf_pages" or .key == "perf_loading" or .key == "perf_client")) + elif .laneId == "infrastructure-network" then .consumedWorklistHashes |= with_entries(select(.key == "perf_route_handlers" or .key == "perf_next_configs" or .key == "perf_large_files")) + else . end + ) +' "$ROOT/tests/fixtures/deep-audit/report.performance-valid.json" >"$deep_audit_lane_scoped_fixture" +assert_command "deep-audit lane receipts accept lane-scoped worklists" node "$ROOT/scripts/deep-audit-artifact-check.mjs" validate --artifact "$deep_audit_lane_scoped_fixture" assert_command "deep-audit source-limited artifact passes" node "$ROOT/scripts/deep-audit-artifact-check.mjs" validate --artifact "$ROOT/tests/fixtures/deep-audit/report.source-limited.json" assert_command "deep-audit fixture suite passes" node "$ROOT/scripts/deep-audit-artifact-check.mjs" validate-fixtures assert_command "deep-audit registry validates" node "$ROOT/scripts/deep-audit-artifact-check.mjs" validate-registry --root "$ROOT" @@ -693,6 +727,7 @@ cat >"$TMPROOT/tool-stack-hindsight/claude-code.json" <<'JSON' JSON tool_stack_json="$(PATH="$tool_stack_bin:/usr/bin:/bin" CLAUDE_HOME="$TMPROOT/tool-stack-home" HINDSIGHT_HOME="$TMPROOT/tool-stack-hindsight" ETRNL_TOOL_STACK_STATE="$TMPROOT/tool-stack-state.json" "$node_bin" "$ROOT/scripts/tool-stack-check.mjs" --json --force)" assert_json_expr "tool stack checker detects codegraph update" "$tool_stack_json" '.tools.codegraph.installed == true and .tools.codegraph.currentVersion == "0.9.9" and .tools.codegraph.latestVersion == "1.0.0" and .tools.codegraph.updateAvailable == true' +assert_json_expr "tool stack checker uses pinned global npm specs" "$tool_stack_json" '(.tools.codegraph.updateCommand | contains("@colbymchenry/codegraph@1.0.1")) and (.tools.beads.updateCommand | contains("@beads/bd@1.0.5"))' assert_json_expr "tool stack checker keeps beads current" "$tool_stack_json" '.tools.beads.installed == true and .tools.beads.currentVersion == "1.0.5" and .tools.beads.updateAvailable == false' assert_json_expr "tool stack checker reports Hindsight plugin posture" "$tool_stack_json" '.tools.hindsight.pluginEnabled == true and .tools.hindsight.pluginInstalled == true and .tools.hindsight.ok == true and .tools.hindsight.mode == "local-daemon"' mkdir -p "$TMPROOT/tool-stack-home/plugins/cache/hindsight/hindsight-memory/0.7.1/hooks" @@ -775,7 +810,7 @@ assert_json_expr "tool-effectiveness codex import sanitizes tool events" "$tool_ assert_json_expr "tool-effectiveness codex import preserves explicit outcomes" "$tool_effectiveness_codex_import_json" '(.events[] | select(.tool == "codegraph") | .eligible == true and .toolUsed == true and .usefulWork == true and .downstreamArtifact == true) and (.events[] | select(.tool == "beads") | .eligible == false and .toolUsed == false and .usefulWork == false and .downstreamArtifact == false)' assert_command "update shell syntax" bash -n "$ROOT/scripts/update.sh" grep -Fq 'install.sh" --preserve-settings' "$ROOT/scripts/update.sh" || fail "update.sh must preserve settings on upgrade" -grep -Fq 'post_upgrade_canary="$ROOT/scripts/post-upgrade-canary.sh"' "$ROOT/scripts/update.sh" || fail "update.sh must assign post_upgrade_canary before use" +grep -Fq "post_upgrade_canary=\"\$ROOT/scripts/post-upgrade-canary.sh\"" "$ROOT/scripts/update.sh" || fail "update.sh must assign post_upgrade_canary before use" auto_update_source="$TMPROOT/auto-update-source" auto_update_home="$TMPROOT/auto-update-home" mkdir -p "$auto_update_source/scripts" "$auto_update_home/etrnl" @@ -788,6 +823,9 @@ printf 'ran\n' >"$CLAUDE_HOME/auto-update-ran" BASH chmod +x "$auto_update_source/scripts/install.sh" "$auto_update_source/scripts/update.sh" printf '%s\n' '# Changelog' '' '## v0.0.1' >"$auto_update_source/CHANGELOG.md" +git -C "$auto_update_source" init -q +git -C "$auto_update_source" add CHANGELOG.md scripts/install.sh scripts/update.sh +git -C "$auto_update_source" -c user.email='test@example.com' -c user.name='test' commit -q -m 'init' jq -n --arg sourceRoot "$auto_update_source" '{ sourceRoot: $sourceRoot, sourceCommit: "unknown", @@ -802,6 +840,28 @@ assert_no_file "update-check opt-out does not run updater" "$auto_update_home/au auto_default_json="$(CLAUDE_HOME="$auto_update_home" CODEX_HOME="$TMPROOT/auto-update-codex" ETRNL_HOME="$auto_update_home" node "$ROOT/scripts/update-check.mjs" --json)" assert_json_expr "update-check auto-runs local updater by default" "$auto_default_json" '.ok == true and .localUpdateAvailable == false and (.autoUpdate | startswith("ETRNL_AUTO_UPDATED "))' assert_file "update-check default auto ran updater" "$auto_update_home/auto-update-ran" +untrusted_auto_source="$TMPROOT/untrusted-auto-source" +untrusted_auto_home="$TMPROOT/untrusted-auto-home" +mkdir -p "$untrusted_auto_source/scripts" "$untrusted_auto_home/etrnl" +printf '%s\n' '#!/usr/bin/env bash' 'exit 0' >"$untrusted_auto_source/scripts/install.sh" +cat >"$untrusted_auto_source/scripts/update.sh" <<'BASH' +#!/usr/bin/env bash +set -Eeuo pipefail +printf 'ran\n' >"$CLAUDE_HOME/auto-update-untrusted-ran" +BASH +chmod +x "$untrusted_auto_source/scripts/install.sh" "$untrusted_auto_source/scripts/update.sh" +printf '%s\n' '# Changelog' '' '## v0.0.1' >"$untrusted_auto_source/CHANGELOG.md" +jq -n --arg sourceRoot "$untrusted_auto_source" '{ + sourceRoot: $sourceRoot, + sourceCommit: "unknown", + sourceCommitShort: "unknown", + sourceVersion: "v0.0.1", + sourceFingerprint: "stale", + settingsMode: "default" +}' >"$untrusted_auto_home/etrnl/install.json" +untrusted_skipped_json="$(CLAUDE_HOME="$untrusted_auto_home" CODEX_HOME="$TMPROOT/untrusted-auto-codex" ETRNL_HOME="$untrusted_auto_home" node "$ROOT/scripts/update-check.mjs" --json)" +assert_json_expr "update-check skips auto-update on untrusted source git state" "$untrusted_skipped_json" '.autoUpdate | startswith("ETRNL_AUTO_UPDATE_SKIPPED untrusted-source-git-state")' +assert_no_file "update-check untrusted skip does not run updater" "$untrusted_auto_home/auto-update-untrusted-ran" dirty_auto_source="$TMPROOT/dirty-auto-source" dirty_auto_home="$TMPROOT/dirty-auto-home" mkdir -p "$dirty_auto_source/scripts" "$dirty_auto_home/etrnl" @@ -1066,6 +1126,13 @@ printf '%s\n' \ '## v0.1.0' '' '2026-01-01' '' '### Added' '' '- Initial release.' \ >"$changelog_repo/CHANGELOG.md" assert_command "changelog check accepts release after tag" node "$ROOT/scripts/changelog-release-check.mjs" --root "$changelog_repo" --skip-version-file +printf '%s\n' '0.1.1' >"$changelog_repo/VERSION" +if pending_release_out="$(node "$ROOT/scripts/changelog-release-check.mjs" --root "$changelog_repo" 2>&1)"; then + not_ok "changelog check rejects untagged pending release" +else + assert_contains "changelog check rejects untagged pending release" "$pending_release_out" "not tagged" +fi +assert_command "changelog check allows pending release with flag" node "$ROOT/scripts/changelog-release-check.mjs" --root "$changelog_repo" --allow-pending-release printf '%s\n' \ '# Changelog' '' '## Unreleased' '' '### Added' '' \ '## v0.1.2' '' '2026-01-03' '' '### Added' '' '- Current pending release.' '' \ @@ -1341,7 +1408,7 @@ bad_plan_json="$(node "$ROOT/scripts/plan-readiness-check.mjs" "$bad_plan" --jso assert_json_expr "plan readiness emits repair hints" "$bad_plan_json" '(.repairHints | length) > 0' tbd_name_plan="$TMPROOT/tbd-name-plan.md" cp "$ROOT/hooks/fixtures/plans/good-plan.md" "$tbd_name_plan" -printf '%s\n' '' '- Pilot repo: agency-tbd backfill.' >>"$tbd_name_plan" +printf '%s\n' '' '- Pilot repo: example-agency-tbd backfill.' >>"$tbd_name_plan" tbd_name_json="$(node "$ROOT/scripts/plan-readiness-check.mjs" "$tbd_name_plan" --json --allow-transitional-deep-stack 2>/dev/null || true)" assert_json_expr "plan readiness allows hyphenated tbd repo names" "$tbd_name_json" '([.failures[].name] | index("tbd")) == null' printf '%s\n' '- Budget: TBD before rollout.' >>"$tbd_name_plan" @@ -1658,12 +1725,33 @@ if [[ -f "$SYNC_SCRIPT" ]]; then else ok "sync --check rejects banned token in source" fi + local_privacy_manifest="$TMPROOT/local-privacy-manifest.json" + local_privacy_tokens="$TMPROOT/privacy-banned-tokens.local" + jq '.privacy.localTokenFiles = ["privacy-banned-tokens.local"]' "$FIXTURE_MANIFEST" >"$local_privacy_manifest" + printf '%s\n' 'LOCAL_PRIVATE_TOKEN' >"$local_privacy_tokens" + local_private_module="$TMPROOT/local-private-fixture.md" + cp "$FIXTURE_RULES_DIR/fixture.md" "$local_private_module" + printf '\nLOCAL_PRIVATE_TOKEN is present here.\n' >>"$local_private_module" + if local_privacy_out="$(node "$SYNC_SCRIPT" --check --source "$local_private_module" --manifest "$local_privacy_manifest" --output "$sync_out_dir2" 2>&1)"; then + not_ok "sync --check rejects local privacy token in source (should have failed)" + else + assert_contains "sync --check rejects local privacy token in source" "$local_privacy_out" "banned token match count=1" + if [[ "$local_privacy_out" == *"LOCAL_PRIVATE_TOKEN"* ]]; then + not_ok "sync --check redacts local privacy token value" + else + ok "sync --check redacts local privacy token value" + fi + fi + assert_command "repo rule sync validates manifest index" node "$SYNC_SCRIPT" --check else not_ok "sync-rule-exports.mjs exists" not_ok "sync emits .mdc file" not_ok "sync --check passes on stable output (idempotent)" not_ok "sync --check fails on drifted .mdc" not_ok "sync --check rejects banned token in source" + not_ok "sync --check rejects local privacy token in source" + not_ok "sync --check redacts local privacy token value" + not_ok "repo rule sync validates manifest index" fi if [[ -f "$INIT_SCRIPT" ]]; then @@ -1672,6 +1760,7 @@ if [[ -f "$INIT_SCRIPT" ]]; then mkdir -p "$init_target" dry_out="$(bash "$INIT_SCRIPT" --profile eternal-saas --dry-run "$init_target" 2>&1)" assert_contains "init dry-run mentions eternal-saas" "$dry_out" "eternal-saas" + assert_contains "init dry-run installs Cursor mdc files" "$dry_out" ".cursor/rules/eternal-saas/project/eternal-saas-orpc.mdc" if [[ -d "$init_target/.claude/rules/eternal-saas" ]]; then not_ok "init --dry-run does not write files" else @@ -1689,12 +1778,27 @@ if [[ -f "$INIT_SCRIPT" ]]; then real_target="$TMPROOT/init-real-target" mkdir -p "$real_target" bash "$INIT_SCRIPT" --profile eternal-saas "$real_target" >/dev/null 2>&1 || true + assert_file "init installs generated Cursor mdc files" "$real_target/.cursor/rules/eternal-saas/project/eternal-saas-orpc.mdc" + assert_no_file "init does not install raw Cursor markdown files" "$real_target/.cursor/rules/eternal-saas/project/orpc.md" + tcg_target="$TMPROOT/init-tcg-target" + mkdir -p "$tcg_target" + bash "$INIT_SCRIPT" --profile eternal-saas-tcg "$tcg_target" >/dev/null 2>&1 || true + assert_file "init tcg profile installs tcg contract module" "$tcg_target/.claude/rules/eternal-saas/project/tcg-contract.md" # simulate manifest bump by touching source (sleep ensures different mtime second) - sleep 1 - touch "$ROOT/rules/eternal-saas/project/orpc.md" - check_out="$(bash "$INIT_SCRIPT" --check --profile eternal-saas "$real_target" 2>&1)" || true - assert_contains "init --check reports stale after manifest bump" "$check_out" "stale" - git -C "$ROOT" checkout -- rules/eternal-saas/project/orpc.md 2>/dev/null || true + orpc_source="$ROOT/rules/eternal-saas/project/orpc.md" + orpc_mtime_marker="$TMPROOT/orpc-source-mtime" + if [[ -f "$orpc_source" ]]; then + touch -r "$orpc_source" "$orpc_mtime_marker" + trap 'touch -r "$orpc_mtime_marker" "$orpc_source" 2>/dev/null || true; cc_test_cleanup' EXIT + sleep 1 + touch "$orpc_source" + check_out="$(bash "$INIT_SCRIPT" --check --profile eternal-saas "$real_target" 2>&1)" || true + assert_contains "init --check reports stale after manifest bump" "$check_out" "stale" + touch -r "$orpc_mtime_marker" "$orpc_source" + trap cc_test_cleanup EXIT + else + not_ok "init --check source fixture exists" + fi # init --check reports locally-modified after target edit target_orpc="$real_target/.claude/rules/eternal-saas/project/orpc.md" From 4c6f90d8a3592932e8b2d5ae1438c442c17617ab Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 13:30:41 -0700 Subject: [PATCH 02/24] fix compact reset boundaries --- hooks/cc-sessionstart-restore.sh | 11 ++++++++++- scripts/lib/etrnl-state-core.mjs | 33 ++++++++++++++++++++++++++++++-- scripts/workflow-health.mjs | 2 +- tests/test-hooks.sh | 22 +++++++++++++++++++++ tests/test-workflow-tools.sh | 10 ++++++++++ 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/hooks/cc-sessionstart-restore.sh b/hooks/cc-sessionstart-restore.sh index 86b23d7..1adef29 100755 --- a/hooks/cc-sessionstart-restore.sh +++ b/hooks/cc-sessionstart-restore.sh @@ -42,9 +42,9 @@ process.stdin.on("end", () => { cc_json_read_stdin cc_json_require_jq || exit 0 cc_json_valid || exit 0 +source_name="$(cc_json_get '.source')" cc_state_init -source_name="$(cc_json_get '.source')" cwd="$(cc_project_cwd)" branch="" dirty="" @@ -53,6 +53,15 @@ if command -v git >/dev/null 2>&1 && git -C "$cwd" rev-parse --is-inside-work-tr dirty="$(git -C "$cwd" status --porcelain 2>/dev/null | wc -l | xargs)" fi cc_state_update --arg cwd "$cwd" ".cwd = \$cwd" +if [[ "$source_name" != "compact" ]]; then + session_event="$(jq -cn \ + --arg session "$(cc_session_id)" \ + --arg cwd "$cwd" \ + --arg source "${source_name:-unknown}" \ + '{eventKind:"session",sessionId:$session,cwd:$cwd,data:{status:"started",source:$source}}')" + cc_etrnl_state_append_json "$session_event" || printf 'claude-guard warning: session-start event append failed\n' >&2 + cc_state_update '.lastCompactSummary = "" | .lastCompactAt = "" | .compactCount = 0' || true +fi skill_hint="$(get_etrnl_skill_hint)" if [[ "$source_name" == "compact" ]]; then diff --git a/scripts/lib/etrnl-state-core.mjs b/scripts/lib/etrnl-state-core.mjs index 8fb971d..329d778 100644 --- a/scripts/lib/etrnl-state-core.mjs +++ b/scripts/lib/etrnl-state-core.mjs @@ -325,20 +325,49 @@ function latestEvent(events, predicate) { })[0] || null; } +function isSessionResetBoundary(event) { + return event.eventKind === "session" && + String(event.data?.status || "") === "started" && + String(event.data?.source || "") !== "compact"; +} + +function eventsAfterLatestSessionReset(events) { + const latestReset = latestEvent(events, isSessionResetBoundary); + if (!latestReset) return events; + const resetSeq = Number(latestReset.eventSeq || 0); + const resetTime = Date.parse(latestReset.at || ""); + return events.filter((event) => { + const seq = Number(event.eventSeq || 0); + if (seq > 0 || resetSeq > 0) return seq > resetSeq; + const eventTime = Date.parse(event.at || ""); + return !Number.isFinite(resetTime) || !Number.isFinite(eventTime) || eventTime > resetTime; + }); +} + +function activeCompactEvents(events) { + const bySession = new Map(); + for (const event of events) { + const key = event.sessionId || ""; + bySession.set(key, [...(bySession.get(key) || []), event]); + } + return [...bySession.values()].flatMap(eventsAfterLatestSessionReset); +} + /** Build the latest compact handoff packet and verification-staleness signal. */ export function compactHandoff(options = {}) { const root = stateRoot(options.stateDir); const events = options.events || readEvents(root); const requestedSession = cleanSessionId(options.session); - const selected = options.latest + const selectedRaw = options.latest ? events : events.filter((event) => requestedSession && event.sessionId === requestedSession); + const selected = options.latest ? activeCompactEvents(selectedRaw) : eventsAfterLatestSessionReset(selectedRaw); const latestCompact = latestEvent(selected, (event) => event.eventKind === "compact_pre" || event.eventKind === "compact_post"); if (!latestCompact) { return { ok: true, found: false, handoff: null, text: "", statePath: statePaths(root).events }; } const sessionId = latestCompact.sessionId; - const sessionEvents = events.filter((event) => event.sessionId === sessionId); + const sessionEvents = eventsAfterLatestSessionReset(events.filter((event) => event.sessionId === sessionId)); const latestPre = latestEvent(sessionEvents, (event) => event.eventKind === "compact_pre"); const latestPost = latestEvent(sessionEvents, (event) => event.eventKind === "compact_post"); const latestCheck = latestEvent(sessionEvents, (event) => event.eventKind === "check" && (event.data.category === "verification" || event.data.verification === true)); diff --git a/scripts/workflow-health.mjs b/scripts/workflow-health.mjs index dd5cb0a..7827ed7 100755 --- a/scripts/workflow-health.mjs +++ b/scripts/workflow-health.mjs @@ -203,7 +203,7 @@ function effectivenessStats() { function compactStats() { try { - const handoff = compactHandoff({ latest: true }); + const handoff = compactHandoff({ session: sessionFilter, latest: !sessionFilter }); return { found: handoff.found, staleVerification: Boolean(handoff.handoff?.verificationStale), diff --git a/tests/test-hooks.sh b/tests/test-hooks.sh index 70e8736..2053245 100755 --- a/tests/test-hooks.sh +++ b/tests/test-hooks.sh @@ -949,6 +949,28 @@ assert_contains "session start injects ETRNL skill hint" "$out" "ETRNL skills" compact_stale_stop="$(jq -cn '{session_id:"fixture-session",last_assistant_message:"Done, tests pass.",stop_hook_active:false}')" out="$(run_hook cc-stop-verifier.sh "$compact_stale_stop")" assert_contains "stop verifier blocks stale compact verification" "$out" "Verification is stale after compact" +session_reset_json="$(jq -cn '{session_id:"fixture-session",hook_event_name:"SessionStart",source:"startup"}')" +out="$(run_hook cc-sessionstart-restore.sh "$session_reset_json")" +if [[ "$out" == *"Compact recovery"* ]]; then + not_ok "session reset does not replay compact recovery: $out" +else + ok "session reset does not replay compact recovery" +fi +assert_json_expr "session reset records durable start boundary" "$(jq -s -c . "$ETRNL_STATE_DIR/events.jsonl")" 'any(.[]; .eventKind == "session" and .sessionId == "fixture-session" and .data.status == "started" and .data.source == "startup")' +reset_handoff_json="$(ETRNL_STATE_DIR="$ETRNL_STATE_DIR" node "$ROOT/scripts/etrnl-state.mjs" compact-handoff --session fixture-session --json)" +assert_json_expr "session reset clears compact handoff for same session id" "$reset_handoff_json" '.found == false and .handoff == null' +printf '%s\n' '{"eventKind":"compact_post","sessionId":"fixture-session","data":{"compactSummary":"before clear","verificationStale":true}}' \ + | ETRNL_STATE_DIR="$ETRNL_STATE_DIR" node "$ROOT/scripts/etrnl-state.mjs" append --json >/dev/null +session_clear_json="$(jq -cn '{session_id:"fixture-session",hook_event_name:"SessionStart",source:"clear"}')" +out="$(run_hook cc-sessionstart-restore.sh "$session_clear_json")" +if [[ "$out" == *"Compact recovery"* ]]; then + not_ok "session clear does not replay compact recovery: $out" +else + ok "session clear does not replay compact recovery" +fi +assert_json_expr "session clear records durable start boundary" "$(jq -s -c . "$ETRNL_STATE_DIR/events.jsonl")" 'any(.[]; .eventKind == "session" and .sessionId == "fixture-session" and .data.status == "started" and .data.source == "clear")' +clear_handoff_json="$(ETRNL_STATE_DIR="$ETRNL_STATE_DIR" node "$ROOT/scripts/etrnl-state.mjs" compact-handoff --session fixture-session --json)" +assert_json_expr "session clear cuts off same-session compact handoff" "$clear_handoff_json" '.found == false and .handoff == null' node "$ROOT/scripts/execution-ledger.mjs" init --session fixture-session-status --plan "$ROOT/hooks/fixtures/plans/good-plan.md" >/dev/null node "$ROOT/scripts/execution-ledger.mjs" set-task --session fixture-session-status --task T1 --title Task --status in_progress diff --git a/tests/test-workflow-tools.sh b/tests/test-workflow-tools.sh index 546fe6f..e8133fb 100755 --- a/tests/test-workflow-tools.sh +++ b/tests/test-workflow-tools.sh @@ -642,6 +642,16 @@ printf '%s\n' '{"eventKind":"compact_pre","sessionId":"single-compact","at":"202 | ETRNL_STATE_DIR="$etrnl_single_compact_dir" node "$ROOT/scripts/etrnl-state.mjs" append --json >/dev/null etrnl_single_compact_json="$(ETRNL_STATE_DIR="$etrnl_single_compact_dir" node "$ROOT/scripts/etrnl-state.mjs" compact-handoff --session single-compact --json)" assert_json_expr "etrnl compact handoff uses one newest compact event" "$etrnl_single_compact_json" '.handoff.task == "pre task" and .handoff.nextAction == "pre next" and .handoff.summary == "pre summary"' +etrnl_reset_dir="$TMPROOT/etrnl-state-reset" +printf '%s\n' '{"eventKind":"compact_post","sessionId":"reset-session","at":"2026-06-05T03:00:00Z","data":{"compactSummary":"before reset","verificationStale":true}}' \ + | ETRNL_STATE_DIR="$etrnl_reset_dir" node "$ROOT/scripts/etrnl-state.mjs" append --json >/dev/null +printf '%s\n' '{"eventKind":"session","sessionId":"reset-session","at":"2026-06-05T04:00:00Z","data":{"status":"started","source":"startup"}}' \ + | ETRNL_STATE_DIR="$etrnl_reset_dir" node "$ROOT/scripts/etrnl-state.mjs" append --json >/dev/null +etrnl_reset_json="$(ETRNL_STATE_DIR="$etrnl_reset_dir" node "$ROOT/scripts/etrnl-state.mjs" compact-handoff --session reset-session --json)" +assert_json_expr "etrnl compact handoff ignores compact before session reset" "$etrnl_reset_json" '.found == false and .handoff == null' +assert_command "etrnl stop-status allows session reset after compact" env ETRNL_STATE_DIR="$etrnl_reset_dir" node "$ROOT/scripts/etrnl-state.mjs" stop-status --session reset-session --json +etrnl_workflow_reset_json="$(ETRNL_STATE_DIR="$etrnl_reset_dir" node "$ROOT/scripts/workflow-health.mjs" status --session reset-session --json)" +assert_json_expr "workflow status scopes compact state to reset session" "$etrnl_workflow_reset_json" '.compact.found == false and .compact.staleVerification == false' if ETRNL_STATE_DIR="$etrnl_state_dir" node "$ROOT/scripts/etrnl-state.mjs" stop-status --session fixture-compact --json >/dev/null 2>&1; then not_ok "etrnl stop-status blocks stale compact verification" else From 57719082c65b2a648223c6d5764aa3b349b34272 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 13:45:40 -0700 Subject: [PATCH 03/24] fix coderabbit review findings --- README.md | 2 +- scripts/lib/etrnl-state-core.mjs | 2 +- tests/test-workflow-tools.sh | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 436b32a..1fdbd01 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Don't skip those steps. Strict mode with untested hooks will interrupt things yo | [CLAUDE.md](CLAUDE.md) | Thin Claude Code wrapper that imports `AGENTS.md` | | [docs/hooks.md](docs/hooks.md) | Hook reference: every `cc-*` entrypoint, events, default vs strict | | [docs/guards.md](docs/guards.md) | Pretool deny catalog, stop-verifier gates, fail-open matrix | -| [docs/rules.md](docs/rules.md) | Cross-host rule pack: module catalog, host activation, `init-project-rules.sh` | +| [docs/rules.md](docs/rules.md) | Claude and Cursor rule pack: module catalog, host activation, `init-project-rules.sh` | | [docs/install.md](docs/install.md) | Install, update, uninstall, profiles, strict mode | | [docs/migration.md](docs/migration.md) | Rollout sequencing — safe migration order | | [docs/configuration.md](docs/configuration.md) | Profiles, strict mode, env var tuning | diff --git a/scripts/lib/etrnl-state-core.mjs b/scripts/lib/etrnl-state-core.mjs index 329d778..1ad9a46 100644 --- a/scripts/lib/etrnl-state-core.mjs +++ b/scripts/lib/etrnl-state-core.mjs @@ -338,7 +338,7 @@ function eventsAfterLatestSessionReset(events) { const resetTime = Date.parse(latestReset.at || ""); return events.filter((event) => { const seq = Number(event.eventSeq || 0); - if (seq > 0 || resetSeq > 0) return seq > resetSeq; + if (seq > 0 && resetSeq > 0) return seq > resetSeq; const eventTime = Date.parse(event.at || ""); return !Number.isFinite(resetTime) || !Number.isFinite(eventTime) || eventTime > resetTime; }); diff --git a/tests/test-workflow-tools.sh b/tests/test-workflow-tools.sh index e8133fb..f301b5e 100755 --- a/tests/test-workflow-tools.sh +++ b/tests/test-workflow-tools.sh @@ -652,6 +652,21 @@ assert_json_expr "etrnl compact handoff ignores compact before session reset" "$ assert_command "etrnl stop-status allows session reset after compact" env ETRNL_STATE_DIR="$etrnl_reset_dir" node "$ROOT/scripts/etrnl-state.mjs" stop-status --session reset-session --json etrnl_workflow_reset_json="$(ETRNL_STATE_DIR="$etrnl_reset_dir" node "$ROOT/scripts/workflow-health.mjs" status --session reset-session --json)" assert_json_expr "workflow status scopes compact state to reset session" "$etrnl_workflow_reset_json" '.compact.found == false and .compact.staleVerification == false' +etrnl_reset_fallback_json="$(node --input-type=module - "$ROOT" <<'JS' +import path from "node:path"; +const root = process.argv[2]; +const { compactHandoff } = await import(path.join(root, "scripts/lib/etrnl-state-core.mjs")); +const result = compactHandoff({ + events: [ + { eventKind: "compact_post", sessionId: "fallback-session", eventSeq: 7, at: "2026-06-05T03:00:00Z", data: { compactSummary: "before reset" } }, + { eventKind: "session", sessionId: "fallback-session", at: "2026-06-05T04:00:00Z", data: { status: "started", source: "startup" } } + ], + session: "fallback-session" +}); +process.stdout.write(JSON.stringify(result)); +JS +)" +assert_json_expr "etrnl compact handoff falls back to timestamps when reset seq is missing" "$etrnl_reset_fallback_json" '.found == false and .handoff == null' if ETRNL_STATE_DIR="$etrnl_state_dir" node "$ROOT/scripts/etrnl-state.mjs" stop-status --session fixture-compact --json >/dev/null 2>&1; then not_ok "etrnl stop-status blocks stale compact verification" else From f37dfde3e50927ed6ef85e63c7c1567707d94c0b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 13:58:59 -0700 Subject: [PATCH 04/24] fix coderabbit follow-up findings --- .github/workflows/health.yml | 2 +- hooks/cc-posttoolusefailure-diagnose.sh | 2 +- hooks/cc-userprompt-router.sh | 4 ++-- scripts/deep-audit-artifact-check.mjs | 4 ++++ scripts/post-upgrade-canary.sh | 2 +- scripts/skill-update-prompt.mjs | 2 -- scripts/tool-stack-check.mjs | 2 ++ skills/bundled/orpc-patterns/agents/openai.yaml | 2 +- tests/test-hooks.sh | 2 +- 9 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml index 59cb790..ab35be7 100644 --- a/.github/workflows/health.yml +++ b/.github/workflows/health.yml @@ -33,7 +33,7 @@ jobs: - name: Install CLI dependencies run: | sudo apt-get update - sudo apt-get install -y jq fd-find ripgrep shellcheck + sudo apt-get install -y jq fd-find ripgrep shellcheck python3 sudo ln -sf "$(command -v fdfind)" /usr/local/bin/fd - name: Validate generated rule exports diff --git a/hooks/cc-posttoolusefailure-diagnose.sh b/hooks/cc-posttoolusefailure-diagnose.sh index bc83ab3..790a87d 100755 --- a/hooks/cc-posttoolusefailure-diagnose.sh +++ b/hooks/cc-posttoolusefailure-diagnose.sh @@ -53,7 +53,7 @@ failure_hint() { return 0 ;; *"triage_guard_ml_disagreed"*|*"ml archive review found"*disagreement*) - printf 'Email-triage ML disagreement is a recoverable guard path, not a question for the repository owner. Inspect the run with etrnl-email triage ml-reviews --latest --account --limit 20, then patch deterministic rules/cache or rerun guarded-run with the exact run id from the runtime output.' + printf 'Email-triage ML disagreement is a recoverable guard path, not a question for the repository owner. Inspect the run with etrnl-email triage ml-reviews --latest --account --limit 20, then patch deterministic rules/cache or rerun guarded-run with the exact run id from the runtime output.' return 0 ;; *"veloz deploy"*|*"vercel deploy"*) diff --git a/hooks/cc-userprompt-router.sh b/hooks/cc-userprompt-router.sh index 800e716..07e304a 100755 --- a/hooks/cc-userprompt-router.sh +++ b/hooks/cc-userprompt-router.sh @@ -332,9 +332,9 @@ if [[ "$prompt_lower" =~ /email-triage|email[[:space:]-]+triage ]]; then email_triage_account="${BASH_REMATCH[2]}" fi if [[ -n "$email_triage_account" ]]; then - notes+=("Use /email-triage as two phases. Phase 1: Inbox Zero first. Run: etrnl-email triage guarded-run --account $email_triage_account --max-inbox 500 --apply --require-insights, then verify with etrnl-email triage verify --latest --account $email_triage_account. Do not open the queue unless verify reports inbox_zero_verified true and inbox_count 0. Phase 2: only after Inbox Zero, paste one generated queue item with etrnl-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") + notes+=("Use /email-triage as two phases. Phase 1: Inbox Zero first. Run: etrnl-email triage guarded-run --account $email_triage_account --max-inbox 500 --apply --require-insights, then verify with etrnl-email triage verify --latest --account $email_triage_account. Do not open the queue unless verify reports inbox_zero_verified true, inbox_count 0, and either gmail_mutated true or queue_ready_without_mutation true. Phase 2: only after Inbox Zero, paste one generated queue item with etrnl-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") else - notes+=("Use /email-triage as two phases. If no account id is present, ask for it. Phase 1: Inbox Zero first with etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Do not open the queue unless verify reports inbox_zero_verified true and inbox_count 0. Phase 2: only after Inbox Zero, paste one generated queue item with etrnl-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") + notes+=("Use /email-triage as two phases. If no account id is present, ask for it. Phase 1: Inbox Zero first with etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Do not open the queue unless verify reports inbox_zero_verified true, inbox_count 0, and either gmail_mutated true or queue_ready_without_mutation true. Phase 2: only after Inbox Zero, paste one generated queue item with etrnl-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") fi fi if [[ "$prompt_lower" =~ disk[[:space:]-]+cleanup|clean[[:space:]]+up[[:space:]]+disk|free[[:space:]]+(disk|ssd|storage)[[:space:]]+space|reclaim[[:space:]]+(disk|ssd|storage)[[:space:]]+space ]]; then diff --git a/scripts/deep-audit-artifact-check.mjs b/scripts/deep-audit-artifact-check.mjs index ef77760..315083c 100644 --- a/scripts/deep-audit-artifact-check.mjs +++ b/scripts/deep-audit-artifact-check.mjs @@ -183,6 +183,10 @@ function validateConsumedHashes(item, category, artifact, artifactPath, errors, } const hashes = worklistHashes(artifact); const worklists = artifact.worklists && typeof artifact.worklists === "object" ? artifact.worklists : {}; + if (!Array.isArray(requiredWorklists)) { + errors.push(diagnostic("REQUIRED_WORKLISTS_INVALID", artifactPath, `${jsonPath} has invalid required worklist metadata.`, "Deep audit category registry entries must expose required or allowed worklists as arrays.", "Fix REGISTERED_DEEP_AUDIT_CATEGORIES so requiredWorklists or allowedWorklists is an array.", jsonPath)); + return; + } for (const worklistId of requiredWorklists) { if (!hasOwn(worklists, worklistId)) { errors.push(diagnostic("REQUIRED_WORKLIST_MISSING", artifactPath, `${jsonPath} cannot find required shared worklist ${worklistId}.`, "Selected categories must consume every required worklist from the orchestrator inventory.", `Add ${worklistId} to worklists with sha256 and artifactLabel.`, `$.worklists.${worklistId}`)); diff --git a/scripts/post-upgrade-canary.sh b/scripts/post-upgrade-canary.sh index b2f7bfd..74eae9f 100644 --- a/scripts/post-upgrade-canary.sh +++ b/scripts/post-upgrade-canary.sh @@ -137,7 +137,7 @@ cat >"$canary_etrnl_email" <<'BASH' #!/usr/bin/env bash if [[ "$1 $2" == "triage verify" ]]; then if [[ "${ETRNL_EMAIL_VERIFY_NONZERO:-0}" == "1" ]]; then - printf '{"ok":true,"data":{"verified":true,"dry_run":false,"gmail_mutated":true,"inbox_zero_verified":true,"inbox_count":1}}\n' + printf '{"ok":true,"data":{"verified":true,"dry_run":false,"gmail_mutated":true,"inbox_zero_verified":false,"inbox_count":1}}\n' elif [[ "${ETRNL_EMAIL_VERIFY_READY:-0}" == "1" ]]; then printf '{"ok":true,"data":{"verified":true,"dry_run":true,"gmail_mutated":false,"inbox_zero_verified":true,"queue_ready_without_mutation":true,"inbox_count":0,"action_backlog_count":31}}\n' else diff --git a/scripts/skill-update-prompt.mjs b/scripts/skill-update-prompt.mjs index c8e44dd..dddf3cf 100644 --- a/scripts/skill-update-prompt.mjs +++ b/scripts/skill-update-prompt.mjs @@ -148,8 +148,6 @@ if (state.sourceGitWarning) warningLines.push(`ETRNL_UPDATE_WARNING ${state.sour const resultOk = state.ok !== false; const promptNeeded = Boolean( !resultOk || - state.localUpdateAvailable || - state.remote?.updateAvailable || missingTools.length || toolUpdates.length || actionLines.length, diff --git a/scripts/tool-stack-check.mjs b/scripts/tool-stack-check.mjs index 815c6c5..edcbc90 100755 --- a/scripts/tool-stack-check.mjs +++ b/scripts/tool-stack-check.mjs @@ -425,6 +425,8 @@ function failedProjectStatus(resolved, message) { } const tools = [ + // Admin-tool install commands interpolate trusted user environment specs. + // Shell metacharacters in ETRNL_*_NPM_SPEC values would execute locally by design. { id: "codegraph", command: "codegraph", diff --git a/skills/bundled/orpc-patterns/agents/openai.yaml b/skills/bundled/orpc-patterns/agents/openai.yaml index abf75bd..8b12f53 100644 --- a/skills/bundled/orpc-patterns/agents/openai.yaml +++ b/skills/bundled/orpc-patterns/agents/openai.yaml @@ -1,6 +1,6 @@ interface: display_name: "oRPC Patterns" short_description: "Build and review oRPC APIs" - default_prompt: "Use $orpc-patterns to review this oRPC API change for current patterns, missing validation, typed errors, and client/query integration issues." + default_prompt: "Use orpc-patterns to review this oRPC API change for current patterns, missing validation, typed errors, and client/query integration issues." policy: allow_implicit_invocation: true diff --git a/tests/test-hooks.sh b/tests/test-hooks.sh index 2053245..6b46afa 100755 --- a/tests/test-hooks.sh +++ b/tests/test-hooks.sh @@ -510,7 +510,7 @@ email_prompt="$(jq -cn '{session_id:"fixture-email-prompt",prompt:"/email-triage out="$(run_hook cc-userprompt-router.sh "$email_prompt")" assert_contains "email prompt emits exact guarded command" "$out" "etrnl-email triage guarded-run --account fixture-account --max-inbox 500 --apply --require-insights" assert_contains "email prompt requires inbox zero verify" "$out" "etrnl-email triage verify --latest --account fixture-account" -assert_contains "email prompt blocks queue before inbox zero" "$out" "Do not open the queue unless verify reports inbox_zero_verified true and inbox_count 0" +assert_contains "email prompt blocks queue before inbox zero" "$out" "Do not open the queue unless verify reports inbox_zero_verified true, inbox_count 0, and either gmail_mutated true or queue_ready_without_mutation true" assert_contains "email prompt emits reply queue command" "$out" "etrnl-email triage queue --run-id --mode reply --format markdown --next" email_prompt_state="$TMPROOT/claude-guard-fixture-email-prompt.json" assert_json_expr "email triage skill recorded" "$(jq -c . "$email_prompt_state")" 'any(.requestedSkills[]?.value; . == "email-triage")' From d575c65b3d4aa0594567db3d8462935a533b3904 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 14:26:49 -0700 Subject: [PATCH 05/24] address coderabbit follow-ups --- .github/workflows/health.yml | 4 ++-- scripts/tool-stack-check.mjs | 3 ++- skills/bundled/ci-cd/SKILL.md | 2 +- .../ci-cd/templates/github-actions/docker-release.yml | 2 +- skills/bundled/code-simplifier/SKILL.md | 9 +++------ 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml index ab35be7..7418eb5 100644 --- a/.github/workflows/health.yml +++ b/.github/workflows/health.yml @@ -20,12 +20,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: persist-credentials: false - name: Set up Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: "22" cache: "" diff --git a/scripts/tool-stack-check.mjs b/scripts/tool-stack-check.mjs index edcbc90..e18cd85 100755 --- a/scripts/tool-stack-check.mjs +++ b/scripts/tool-stack-check.mjs @@ -426,7 +426,8 @@ function failedProjectStatus(resolved, message) { const tools = [ // Admin-tool install commands interpolate trusted user environment specs. - // Shell metacharacters in ETRNL_*_NPM_SPEC values would execute locally by design. + // Set ETRNL_*_NPM_SPEC only from trusted administrator-controlled input; + // shell metacharacters in those values would execute locally by design. { id: "codegraph", command: "codegraph", diff --git a/skills/bundled/ci-cd/SKILL.md b/skills/bundled/ci-cd/SKILL.md index 18c5c04..8830865 100644 --- a/skills/bundled/ci-cd/SKILL.md +++ b/skills/bundled/ci-cd/SKILL.md @@ -115,7 +115,7 @@ Measure before and after. Report the long pole and the wall-clock critical path, - Use multi-stage Dockerfiles, copy dependency manifests before source for layer caching, and keep `.dockerignore` tight. - Run as non-root, set health checks where appropriate, avoid secrets in `ARG`/`ENV`, and avoid `:latest` base images in production. - Use BuildKit cache (`cache-from`/`cache-to`) and keep cache scopes stable but not over-broad. -- Scan images with tools such as Trivy/Grype. Start in reference-only mode when a legacy image is noisy, then make confirmed high/critical exploitable findings blocking once the initial baseline is triaged. +- Scan images with tools such as Trivy/Grype. Start in reference-only mode when a legacy image is noisy, then make confirmed high/critical exploitable findings blocking once a baseline artifact or tracked issue lists each finding with owner, disposition, and target date. - Generate SBOMs and provenance for release artifacts when the project is production-facing. ## DevSecOps and Supply Chain diff --git a/skills/bundled/ci-cd/templates/github-actions/docker-release.yml b/skills/bundled/ci-cd/templates/github-actions/docker-release.yml index 25aaa53..d35bd78 100644 --- a/skills/bundled/ci-cd/templates/github-actions/docker-release.yml +++ b/skills/bundled/ci-cd/templates/github-actions/docker-release.yml @@ -60,4 +60,4 @@ jobs: with: image-ref: ${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} severity: HIGH,CRITICAL - exit-code: "1" + exit-code: "0" diff --git a/skills/bundled/code-simplifier/SKILL.md b/skills/bundled/code-simplifier/SKILL.md index 63ba417..c53ae0c 100644 --- a/skills/bundled/code-simplifier/SKILL.md +++ b/skills/bundled/code-simplifier/SKILL.md @@ -13,13 +13,10 @@ You will analyze recently modified code and apply refinements that: 1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact. -2. **Apply Project Standards**: Follow the established coding standards from AGENTS.md including: - - Use ES modules with proper import sorting and extensions - - Use `function` declarations for exported or top-level named functions; arrow functions remain acceptable for callbacks, closures, React hook callbacks, and inline handlers - - Use explicit return type annotations for top-level functions - - Follow proper React component patterns with explicit Props types +2. **Apply Project Standards**: Follow the repo's enforced coding standards from AGENTS.md, lint/type/test configuration, and local examples: + - Use the module system, import style, and naming conventions already present in the target package + - Preserve explicit types where the local TypeScript, lint, or component patterns already require them - Use proper error handling patterns and avoid unnecessary exception-handling blocks - - Maintain consistent naming conventions 3. **Enhance Clarity**: Simplify code structure by: - Reducing unnecessary complexity and nesting From 96969cfafc9fb1ae40fac03825e5faa7d70e5bb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 14:46:13 -0700 Subject: [PATCH 06/24] resolve coderabbit readability notes --- rules-manifest.json | 2 +- scripts/skill-update-prompt.mjs | 8 +++++++- .../ci-cd/templates/github-actions/docker-release.yml | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/rules-manifest.json b/rules-manifest.json index abe4461..bee2e7c 100644 --- a/rules-manifest.json +++ b/rules-manifest.json @@ -59,7 +59,7 @@ "id": "Unique module identifier matching the frontmatter id field.", "paths": "Array of glob patterns for Claude Code paths: frontmatter scoping.", "globs": "Array of glob patterns for Cursor .mdc globs frontmatter.", - "hosts": "Array of target hosts for project rule modules: claude, cursor.", + "hosts": "Array of target hosts for rule modules: claude, cursor.", "maxBytes": "Optional per-module byte cap; doctor warns when exceeded.", "profile": "Profile or array of profiles this module belongs to.", "verify": "Shell command to verify the rule is effective in the target repo.", diff --git a/scripts/skill-update-prompt.mjs b/scripts/skill-update-prompt.mjs index dddf3cf..58c731b 100644 --- a/scripts/skill-update-prompt.mjs +++ b/scripts/skill-update-prompt.mjs @@ -123,6 +123,12 @@ const bootstrapCommand = state.sourceRoot : ""; const actionLines = []; const warningLines = []; +function isToolMissing(tool) { + if (tool.kind === "claude-plugin") { + return tool.pluginEnabled && !tool.pluginInstalled; + } + return tool.installed === false; +} if (state.localUpdateAvailable) { actionLines.push( `ETRNL_UPDATE_AVAILABLE installed=${state.installedCommitShort || "unknown"} source=${state.sourceCommitShort || "unknown"} version=${state.sourceVersion || "unknown"} run="${updateCommand}"`, @@ -138,7 +144,7 @@ for (const tool of Object.values(toolStack.tools || {})) { actionLines.push( `TOOL_STACK_UPDATE_AVAILABLE ${tool.id} current=${tool.currentVersion} latest=${tool.latestVersion} run="${tool.updateCommand}"`, ); - } else if (tool && (tool.kind === "claude-plugin" ? tool.pluginEnabled && !tool.pluginInstalled : tool.installed === false)) { + } else if (tool && isToolMissing(tool)) { actionLines.push(`TOOL_STACK_MISSING ${tool.id} install="${tool.installCommand}"`); } } diff --git a/skills/bundled/ci-cd/templates/github-actions/docker-release.yml b/skills/bundled/ci-cd/templates/github-actions/docker-release.yml index d35bd78..16bf61f 100644 --- a/skills/bundled/ci-cd/templates/github-actions/docker-release.yml +++ b/skills/bundled/ci-cd/templates/github-actions/docker-release.yml @@ -60,4 +60,5 @@ jobs: with: image-ref: ${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} severity: HIGH,CRITICAL + # Reference-only until a baseline artifact or tracked issue lists confirmed exploitable findings with owners, dispositions, and target dates; then switch to exit-code: "1". exit-code: "0" From a815b6438c2f597abe1dbf5261b587fffa2f643a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 15:05:53 -0700 Subject: [PATCH 07/24] address coderabbit audit notes --- CREDITS.md | 2 +- commands/email-triage.md | 4 ++-- docs/eternal-stack-coverage.md | 2 +- docs/skills.md | 2 ++ scripts/code-health-ledger-check.mjs | 3 ++- scripts/deep-audit-artifact-check.mjs | 4 ++-- scripts/doctor.sh | 6 ++++- scripts/init-project-rules.sh | 12 ++++++---- scripts/lib/etrnl-state-core.mjs | 11 +++++---- skills/bundled/stripe-best-practices/SKILL.md | 24 +++++++++---------- tests/test-workflow-tools.sh | 24 +++++++++++++++++++ 11 files changed, 66 insertions(+), 28 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 3e4893f..116e3bf 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -28,7 +28,7 @@ Eternal Stack is designed as a complete skill family. Policy, review, simplifica | `better-auth`, `tenant-isolation-patterns`, `money-vo-discipline` | Auth, tenancy, and money discipline | Community / upstream skill bundles | | `stripe-best-practices`, `abacatepay-integration` | Payments review | Community / upstream skill bundles | | `ci-cd` | CI helper scripts referenced by `/etrnl-dev-ci` | Community skill bundle | -| `domain-cli`, `domain-cloud-native`, `domain-embedded`, `domain-fintech`, `domain-iot`, `domain-ml`, `domain-web` | Domain-specific review gates | Community / upstream skill bundles | +| `domain-cli`, `domain-cloud-native`, `domain-embedded`, `domain-fintech`, `domain-iot`, `domain-ml`, `domain-web`, `rules/eternal-saas/project/tcg-contract.md` | Domain-specific review gates and TCG project rule routing | Community / upstream skill bundles plus Eternal Stack rules | | `prisma-expert` | Prisma schema and query review | Community / upstream skill bundle | | `i18n-localization` | Locale and translation review | Community / upstream skill bundle | diff --git a/commands/email-triage.md b/commands/email-triage.md index 9b866d5..c5d2e85 100644 --- a/commands/email-triage.md +++ b/commands/email-triage.md @@ -8,8 +8,8 @@ Account argument from the slash command: `$ARGUMENTS` Treat the argument as the account id. If it is empty, ask the repository owner for the account id and stop. -Do not handwrite Gmail commands. Do not send email. Do not mutate Gmail outside the managed email runtime. -Do not run `etrnl-email triage run` for this slash command. That is a dry classification path and does not clear INBOX. +Do not handwrite Gmail commands, send email, or mutate Gmail outside the managed email runtime. +Avoid `etrnl-email triage run` for this slash command; it is a dry classification path and does not clear INBOX. Phase 1 is Inbox Zero. Triage every email in INBOX, archive known bad-quality emails, label action/waiting/manual-review items, remove them from INBOX, and provider-verify INBOX is zero: diff --git a/docs/eternal-stack-coverage.md b/docs/eternal-stack-coverage.md index 1356402..929b7e7 100644 --- a/docs/eternal-stack-coverage.md +++ b/docs/eternal-stack-coverage.md @@ -15,7 +15,7 @@ Status key: `done` means implemented in this repo; `live-gated` means intentiona | Durable artifacts | done: deep-stack bundle, sanitized source manifest, skill matrix, review phase records, TDD evidence, reuse inventory and reuse bindings, findings ledger, completion audit and completion reconciliation, TypeScript trigger evidence, install proof, Hybrid risk tier, review log, browser QA v1/v2 reports with mandatory console/network summaries, route/viewport matrix counts, screenshot hashes, capture freshness, and provenance for completed v2 runs, context save/restore, artifact-required ledger checks, redacted cross-session project buglog hints, workflow-health summaries/status JSON, and local tool-effectiveness verdict summaries. | | Agent templates | done: default-installed executor/reviewer/investigator/scout/adversary/design/DX/browser QA `etrnl-*` agents. | | Shared startup guidance | done: public `AGENTS.md` template plus tiny Claude wrapper importing it. | -| Rules | done: namespaced `rules/etrnl/*.md` to avoid clobbering existing user rules; cross-host eternal-saas pack (`rules/eternal-saas/global/` + `rules/eternal-saas/project/`) with `rules-manifest.json` (schema v1, checksums, privacy gates); `scripts/init-project-rules.sh` deploys pack to any target repo with Claude+Cursor outputs; `scripts/sync-rule-exports.mjs` keeps `.mdc` Cursor twins in sync; doctor byte gate prevents Codex startup overflow. Reference: `docs/rules.md`. | +| Rules | done: namespaced `rules/etrnl/*.md` to avoid clobbering existing user rules; cross-host eternal-saas pack (`rules/eternal-saas/global/` + `rules/eternal-saas/project/`, including `eternal-saas-tcg` and `project/tcg-contract`) with `rules-manifest.json` (schema v1, checksums, privacy gates); `scripts/init-project-rules.sh` deploys pack to any target repo with Claude+Cursor outputs; `scripts/sync-rule-exports.mjs` keeps `.mdc` Cursor twins in sync; doctor byte gate prevents Codex startup overflow. Reference: `docs/rules.md`. | | Install/update/rollback/doctor | done: scripts, tests, rollback (including `rules/eternal-saas` + Codex startup file restore), canaries, rules, docs, templates, agents, settings audit/repair, installed update metadata, drift explain output, artifact helpers, post-upgrade browser-QA canary, installed-home doctor, and workflow-health helpers are installed or checked. | | Hindsight memory consolidation | live-gated: canary verifies strict config; actual migration/removal of competing memory systems remains a personal live operation. | | Plugin/MCP cleanup | live-gated: plan requires inventory and explicit local rollout before removing plugins, MCPs, or permissions. | diff --git a/docs/skills.md b/docs/skills.md index 6afa91b..08029e8 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -147,6 +147,8 @@ Eternal Stack installs two cooperating layers: Bundled skills are vendored under `skills/bundled//` in this repository. `scripts/install.sh` copies each tree to `~/.claude/skills/` and `~/.codex/skills/`. Maintainers refresh vendored copies from canonical host trees with `scripts/vendor-bundled-skills.sh`. +The `eternal-saas-tcg` rule profile adds `rules/eternal-saas/project/tcg-contract.md` for TCG repositories. That rule routes TCG-domain prompts into the same bundled policy and domain-skill enumeration as `eternal-best-practices` and the `domain-*` skills. + When the same guidance exists under `skills/etrnl-*/references/`, prefer the repo module first; load the bundled skill when the task needs the full surface or hooks require it by name. Hindsight is not an ETRNL execution skill and is not compact handoff authority. It is optional semantic recall/export behind `scripts/canary-hindsight.sh`; accepted lessons are first stored as ETRNL `lesson` events. diff --git a/scripts/code-health-ledger-check.mjs b/scripts/code-health-ledger-check.mjs index 43510af..f5d7ab8 100644 --- a/scripts/code-health-ledger-check.mjs +++ b/scripts/code-health-ledger-check.mjs @@ -2,6 +2,7 @@ import { readStdinJson } from "./lib/read-stdin.mjs"; const CODE_HEALTH_SKILL = "code-health"; +const FULL_CODEBASE_AUDIT_PATTERN = /\b(code[- ]health|repo[- ]health|codebase[- ]health|no\s+skips|whole\s+codebase\s+audit|entire\s+codebase\s+audit)\b/; const TERMINAL_DISPOSITIONS = new Set([ "fixed", "false_positive_with_evidence", @@ -65,7 +66,7 @@ function latestCodeHealthRequest(state) { .map((item) => parseStamp(stamp(item))) .filter(Number.isFinite); const prompt = String(state.lastPrompt || "").toLowerCase(); - if (/code[- ]health|repo[- ]health|codebase[- ]health|no skips|whole codebase audit|entire codebase audit/.test(prompt)) { + if (FULL_CODEBASE_AUDIT_PATTERN.test(prompt)) { const startedAt = parseStamp(state.startedAt); if (Number.isFinite(startedAt)) { times.push(startedAt); diff --git a/scripts/deep-audit-artifact-check.mjs b/scripts/deep-audit-artifact-check.mjs index 315083c..38a5d46 100644 --- a/scripts/deep-audit-artifact-check.mjs +++ b/scripts/deep-audit-artifact-check.mjs @@ -184,7 +184,7 @@ function validateConsumedHashes(item, category, artifact, artifactPath, errors, const hashes = worklistHashes(artifact); const worklists = artifact.worklists && typeof artifact.worklists === "object" ? artifact.worklists : {}; if (!Array.isArray(requiredWorklists)) { - errors.push(diagnostic("REQUIRED_WORKLISTS_INVALID", artifactPath, `${jsonPath} has invalid required worklist metadata.`, "Deep audit category registry entries must expose required or allowed worklists as arrays.", "Fix REGISTERED_DEEP_AUDIT_CATEGORIES so requiredWorklists or allowedWorklists is an array.", jsonPath)); + errors.push(diagnostic("REQUIRED_WORKLISTS_INVALID", artifactPath, `${jsonPath} has invalid required worklist metadata.`, "Category and lane worklist metadata must be arrays.", "Fix category.requiredWorklists or lane.allowedWorklists so the value is an array.", jsonPath)); return; } for (const worklistId of requiredWorklists) { @@ -395,7 +395,7 @@ function validateLaneReceipts(artifact, artifactPath, errors, selected) { if (!receipt.summary) { errors.push(diagnostic("LANE_RECEIPT_SUMMARY_MISSING", artifactPath, `${category.categoryId}/${lane.laneId} has no summary.`, "Fanout receipts need a human-readable completion summary before synthesis.", "Add a non-empty summary.", `${receiptPath}.summary`)); } - validateConsumedHashes(receipt, category, artifact, artifactPath, errors, receiptPath, lane.allowedWorklists || category.requiredWorklists); + validateConsumedHashes(receipt, category, artifact, artifactPath, errors, receiptPath, lane.allowedWorklists ?? category.requiredWorklists); } } } diff --git a/scripts/doctor.sh b/scripts/doctor.sh index fe41b82..1c3de92 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -854,7 +854,11 @@ for rel_path in manifest.get("privacy", {}).get("localTokenFiles", []): if not local_path.exists(): continue if local_path.suffix == ".json": - parsed = json.loads(local_path.read_text()) + try: + parsed = json.loads(local_path.read_text()) + except json.JSONDecodeError as error: + print(f"malformed JSON in {rel_path}: {error}", file=sys.stderr) + sys.exit(1) if isinstance(parsed, list): local_tokens = parsed else: diff --git a/scripts/init-project-rules.sh b/scripts/init-project-rules.sh index d39b0a9..b8bc2d9 100755 --- a/scripts/init-project-rules.sh +++ b/scripts/init-project-rules.sh @@ -147,13 +147,14 @@ cursor_rel_for() { if [[ "$DRY_RUN" -eq 1 && "$CHECK_MODE" -eq 0 ]]; then echo "dry-run: profile=$PROFILE target=$TARGET" echo "planned copies to $TARGET:" + modules_output="$(collect_modules)" || exit 1 while IFS=$'\t' read -r key id; do [[ -n "$key" && -n "$id" ]] || continue rel="$key.md" cursor_rel="$(cursor_rel_for "$key" "$id")" echo " copy -> .claude/rules/eternal-saas/$rel" echo " copy -> .cursor/rules/eternal-saas/$cursor_rel" - done < <(collect_modules) + done <<<"$modules_output" echo "planned receipt: $MANIFEST_RECEIPT" exit 0 fi @@ -170,6 +171,7 @@ if [[ "$CHECK_MODE" -eq 1 ]]; then any_stale=0 any_modified=0 + modules_output="$(collect_modules)" || exit 1 while IFS=$'\t' read -r key id; do [[ -n "$key" && -n "$id" ]] || continue rel="$key.md" @@ -214,7 +216,7 @@ if [[ "$CHECK_MODE" -eq 1 ]]; then any_modified=1 fi fi - done < <(collect_modules) + done <<<"$modules_output" if [[ "$any_modified" -gt 0 || "$any_stale" -gt 0 ]]; then exit 1 @@ -226,6 +228,7 @@ if [[ -f "$MANIFEST_RECEIPT" && "$FORCE" -eq 0 ]]; then receipt_sums="$(receipt_checksums checksums)" cursor_receipt_sums="$(receipt_checksums cursorChecksums)" modified_files=() + modules_output="$(collect_modules)" || exit 1 while IFS=$'\t' read -r key id; do [[ -n "$key" && -n "$id" ]] || continue rel="$key.md" @@ -249,7 +252,7 @@ if [[ -f "$MANIFEST_RECEIPT" && "$FORCE" -eq 0 ]]; then modified_files+=("$cursor_rel") fi fi - done < <(collect_modules) + done <<<"$modules_output" if [[ "${#modified_files[@]}" -gt 0 ]]; then echo "error: locally-modified files would be overwritten. Use --force to proceed:" >&2 for file in "${modified_files[@]}"; do echo " $file" >&2; done @@ -262,6 +265,7 @@ checksums_tmp="$(mktemp)" cursor_checksums_tmp="$(mktemp)" trap 'rm -f "$checksums_tmp" "$cursor_checksums_tmp"' EXIT +modules_output="$(collect_modules)" || exit 1 while IFS=$'\t' read -r key id; do [[ -n "$key" && -n "$id" ]] || continue rel="$key.md" @@ -280,7 +284,7 @@ while IFS=$'\t' read -r key id; do printf '%s\t%s\n' "$rel" "$sum" >> "$checksums_tmp" printf '%s\t%s\n' "$cursor_rel" "$cursor_sum" >> "$cursor_checksums_tmp" echo "installed: $rel" -done < <(collect_modules) +done <<<"$modules_output" mkdir -p "$(dirname "$MANIFEST_RECEIPT")" python3 - "$MANIFEST_RECEIPT" "$PROFILE" "$install_ts" "$checksums_tmp" "$cursor_checksums_tmp" <<'PYEOF' diff --git a/scripts/lib/etrnl-state-core.mjs b/scripts/lib/etrnl-state-core.mjs index 1ad9a46..86f476b 100644 --- a/scripts/lib/etrnl-state-core.mjs +++ b/scripts/lib/etrnl-state-core.mjs @@ -337,10 +337,12 @@ function eventsAfterLatestSessionReset(events) { const resetSeq = Number(latestReset.eventSeq || 0); const resetTime = Date.parse(latestReset.at || ""); return events.filter((event) => { - const seq = Number(event.eventSeq || 0); - if (seq > 0 && resetSeq > 0) return seq > resetSeq; const eventTime = Date.parse(event.at || ""); - return !Number.isFinite(resetTime) || !Number.isFinite(eventTime) || eventTime > resetTime; + if (Number.isFinite(resetTime) && Number.isFinite(eventTime) && eventTime !== resetTime) { + return eventTime > resetTime; + } + const seq = Number(event.eventSeq || 0); + return seq > resetSeq; }); } @@ -348,7 +350,8 @@ function activeCompactEvents(events) { const bySession = new Map(); for (const event of events) { const key = event.sessionId || ""; - bySession.set(key, [...(bySession.get(key) || []), event]); + if (!bySession.has(key)) bySession.set(key, []); + bySession.get(key).push(event); } return [...bySession.values()].flatMap(eventsAfterLatestSessionReset); } diff --git a/skills/bundled/stripe-best-practices/SKILL.md b/skills/bundled/stripe-best-practices/SKILL.md index 56f3b45..fd72ce4 100644 --- a/skills/bundled/stripe-best-practices/SKILL.md +++ b/skills/bundled/stripe-best-practices/SKILL.md @@ -7,28 +7,28 @@ category: universal --- # Stripe Best Practices -When designing an integration, always start from the documentation in [Stripe's Integration Options doc](https://docs.stripe.com/payments/payment-methods/integration-options.md) -The [API Tour](https://docs.stripe.com/payments-api/tour.md) -Use the [Go Live Checklist](https://docs.stripe.com/get-started/checklist/go-live.md) before going live. +When designing an integration, always start from the documentation in [Stripe's Integration Options doc](https://docs.stripe.com/payments/payment-methods/integration-options) +The [API Tour](https://docs.stripe.com/payments-api/tour) +Use the [Go Live Checklist](https://docs.stripe.com/get-started/checklist/go-live) before going live. Use explicit Stripe API and SDK versions. Apply monthly backward-compatible releases after tests pass, and plan/test migrations before major-version upgrades. -Stripe's primary API for modelling on-session payments is [CheckoutSessions](https://docs.stripe.com/api/checkout/sessions.md). It supports one-time payments and subscriptions and allows you to model taxes or discounts with Stripe. Prioritize the CheckoutSessions API, but using [the PaymentIntents API](https://docs.stripe.com/payments/paymentintents/lifecycle.md) is also acceptable for off-session payments or when you need lower-level control and will own the checkout flow yourself. Integrations must only use CheckoutSessions, PaymentIntents, SetupIntents, or solutions like Invoicing, Payment Links, or the subscription APIs. +Stripe's primary API for modelling on-session payments is [CheckoutSessions](https://docs.stripe.com/api/checkout/sessions). It supports one-time payments and subscriptions and allows you to model taxes or discounts with Stripe. Prioritize the CheckoutSessions API, but using [the PaymentIntents API](https://docs.stripe.com/payments/paymentintents/lifecycle) is also acceptable for off-session payments or when you need lower-level control and will own the checkout flow yourself. Integrations must only use CheckoutSessions, PaymentIntents, SetupIntents, or solutions like Invoicing, Payment Links, or the subscription APIs. -Never direct users to the Charges API. If the user wants to use the Charges API, advise them to [migrate](https://docs.stripe.com/payments/payment-intents/migration/charges.md) to Checkout Sessions or Payment Intents. +Never direct users to the Charges API. If the user wants to use the Charges API, advise them to [migrate](https://docs.stripe.com/payments/payment-intents/migration/charges) to Checkout Sessions or Payment Intents. -For web integrations, the primary surface is [Checkout](https://docs.stripe.com/payments/checkout.md), a Stripe-hosted or embedded payment page that uses the CheckoutSessions API. For custom UI, the [Payment Element](https://docs.stripe.com/payments/payment-element.md) is an acceptable alternative; when using it, prioritize the CheckoutSessions API over the PaymentIntents API when available. +For web integrations, the primary surface is [Checkout](https://docs.stripe.com/payments/checkout), a Stripe-hosted or embedded payment page that uses the CheckoutSessions API. For custom UI, the [Payment Element](https://docs.stripe.com/payments/payment-element) is an acceptable alternative; when using it, prioritize the CheckoutSessions API over the PaymentIntents API when available. -Never direct users to the legacy Card Element or the Payment Element in card mode, and advise users who ask for the Card Element to [migrate](https://docs.stripe.com/payments/payment-element/migration.md). +Never direct users to the legacy Card Element or the Payment Element in card mode, and advise users who ask for the Card Element to [migrate](https://docs.stripe.com/payments/payment-element/migration). -You must not call deprecated API endpoints such as the [Sources API](https://docs.stripe.com/api/sources/object.md) and avoid outdated API endpoints such as [Tokens](https://docs.stripe.com/api/tokens.md) and [Charges](https://docs.stripe.com/api/charges/create.md) unless there is a specific need and absolutely no other way. Advise using [the Setup Intent API](https://docs.stripe.com/api/setup_intents.md) to save a payment method for a customer; never direct users to the Sources API to save cards to customers for later use. +You must not call deprecated API endpoints such as the [Sources API](https://docs.stripe.com/api/sources/object) and avoid outdated API endpoints such as [Tokens](https://docs.stripe.com/api/tokens) and [Charges](https://docs.stripe.com/api/charges/create) unless there is a specific need and absolutely no other way. Advise using [the Setup Intent API](https://docs.stripe.com/api/setup_intents) to save a payment method for a customer; never direct users to the Sources API to save cards to customers for later use. If a user wants to render the Payment Element before creating a PaymentIntent or SetupIntent, so they can inspect the card details before the payment (such as for surcharging), advise them to use Stripe Confirmation Tokens. Don't direct them to call the createPaymentMethod or createToken Stripe.js functions. -Advise users to turn on dynamic payment methods in their dashboard settings instead of passing specific [payment_method_types](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-payment_method_types.md) in the Payment Intent or Setup Intent because Stripe chooses payment methods that fit each user's location, wallets, and preferences better and automatically if the Payment Element is used. +Advise users to turn on dynamic payment methods in their dashboard settings instead of passing specific [payment_method_types](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-payment_method_types) in the Payment Intent or Setup Intent because Stripe chooses payment methods that fit each user's location, wallets, and preferences better and automatically if the Payment Element is used. -If a PCI-compliant user asks about an integration where they send server-side raw PAN data, state that they can be required to prove PCI compliance to gain access to such an option, for example [payment_method_data](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-payment_method_data.md). Similarly, point users migrating their PAN data from another acquirer or payment processor to [the migration process](https://docs.stripe.com/get-started/data-migrations/pan-import.md). +If a PCI-compliant user asks about an integration where they send server-side raw PAN data, state that they can be required to prove PCI compliance to gain access to such an option, for example [payment_method_data](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-payment_method_data). Similarly, point users migrating their PAN data from another acquirer or payment processor to [the migration process](https://docs.stripe.com/get-started/data-migrations/pan-import). -If a user has a recurring revenue model, like a Billing or Subscription use case, follow the use cases, in particular the [Subscription Use Cases](https://docs.stripe.com/billing/subscriptions/use-cases.md) like [SaaS](https://docs.stripe.com/saas.md). If these apply to the user, direct them to Billing APIs to [plan your integration](https://docs.stripe.com/billing/subscriptions/designing-integration.md) instead of a direct PaymentIntent integration. Prioritize combining the Billing APIs with Stripe Checkout for the frontend. +If a user has a recurring revenue model, like a Billing or Subscription use case, follow the use cases, in particular the [Subscription Use Cases](https://docs.stripe.com/billing/subscriptions/use-cases) like [SaaS](https://docs.stripe.com/saas). If these apply to the user, direct them to Billing APIs to [plan your integration](https://docs.stripe.com/billing/subscriptions/designing-integration) instead of a direct PaymentIntent integration. Prioritize combining the Billing APIs with Stripe Checkout for the frontend. -If a user wants to build a platform using Stripe Connect to manage fund flows, follow [the documented integration types](https://docs.stripe.com/connect/integration-recommendations.md). Direct charges and destination charges have different settlement, dispute, and negative-balance behavior based on the full Connect configuration; do not frame risk as determined by charge type alone. For destination charges, use `on_behalf_of` when the connected account must be both the settlement merchant and business of record; omit `on_behalf_of` when the platform remains the business of record. Never direct users to mix charge types. If the user wants to decide on the specific risk features they must [follow the integration guide](https://docs.stripe.com/connect/design-an-integration.md). Don't direct users to the outdated terms for Connect types like Standard, Express and Custom but always [refer to controller properties](https://docs.stripe.com/connect/migrate-to-controller-properties.md) for the platform and [capabilities](https://docs.stripe.com/connect/account-capabilities.md) for the connected accounts. +If a user wants to build a platform using Stripe Connect to manage fund flows, follow [the documented integration types](https://docs.stripe.com/connect/integration-recommendations). Direct charges and destination charges have different settlement, dispute, and negative-balance behavior based on the full Connect configuration; do not frame risk as determined by charge type alone. For destination charges, use `on_behalf_of` when the connected account must be both the settlement merchant and business of record; omit `on_behalf_of` when the platform remains the business of record. Never direct users to mix charge types. If the user wants to decide on the specific risk features they must [follow the integration guide](https://docs.stripe.com/connect/design-an-integration). Don't direct users to the outdated terms for Connect types like Standard, Express and Custom but always [refer to controller properties](https://docs.stripe.com/connect/migrate-to-controller-properties) for the platform and [capabilities](https://docs.stripe.com/connect/account-capabilities) for the connected accounts. diff --git a/tests/test-workflow-tools.sh b/tests/test-workflow-tools.sh index f301b5e..77b2aca 100755 --- a/tests/test-workflow-tools.sh +++ b/tests/test-workflow-tools.sh @@ -223,6 +223,9 @@ if [[ "$code_health_bad_status" == "missing-inventory" ]]; then ok "code health code_health_prompt_only_state="$(jq -nc '{requestedSkills:[],successfulCommands:[],verificationRuns:[],lastPrompt:"audit the entire codebase with no skips",startedAt:"2026-01-01T00:00:00Z"}')" code_health_prompt_only_status="$(jq -cn --argjson state "$code_health_prompt_only_state" --arg message "Done, code looks fine." '{state:$state,message:$message}' | node "$ROOT/scripts/code-health-ledger-check.mjs")" if [[ "$code_health_prompt_only_status" == "missing-inventory" ]]; then ok "code health checker blocks prompt-only audit bypass"; else not_ok "code health checker blocks prompt-only audit bypass: $code_health_prompt_only_status"; fi +code_health_substring_state="$(jq -nc '{requestedSkills:[],successfulCommands:[],verificationRuns:[],lastPrompt:"decode-health labels are unrelated",startedAt:"2026-01-01T00:00:00Z"}')" +code_health_substring_status="$(jq -cn --argjson state "$code_health_substring_state" --arg message "Done." '{state:$state,message:$message}' | node "$ROOT/scripts/code-health-ledger-check.mjs")" +if [[ -z "$code_health_substring_status" ]]; then ok "code health checker ignores substring prompt matches"; else not_ok "code health checker ignores substring prompt matches: $code_health_substring_status"; fi code_health_state="$(jq -nc '{requestedSkills:[{value:"etrnl-audit-code",at:"2026-01-01T00:00:00Z"}],successfulCommands:[{value:"node ~/.claude/scripts/code-health-inventory.mjs --json --include-untracked",at:"2026-01-01T00:00:01Z"},{value:"tests/test-workflow-tools.sh",at:"2026-01-01T00:00:02Z"}],verificationRuns:[{value:"tests/test-workflow-tools.sh",at:"2026-01-01T00:00:02Z"}]}')" code_health_shallow_status="$(jq -cn --argjson state "$code_health_state" --arg message "Done, code looks fine." '{state:$state,message:$message}' | node "$ROOT/scripts/code-health-ledger-check.mjs")" @@ -667,6 +670,21 @@ process.stdout.write(JSON.stringify(result)); JS )" assert_json_expr "etrnl compact handoff falls back to timestamps when reset seq is missing" "$etrnl_reset_fallback_json" '.found == false and .handoff == null' +etrnl_reset_time_primary_json="$(node --input-type=module - "$ROOT" <<'JS' +import path from "node:path"; +const root = process.argv[2]; +const { compactHandoff } = await import(path.join(root, "scripts/lib/etrnl-state-core.mjs")); +const result = compactHandoff({ + events: [ + { eventKind: "session", sessionId: "time-primary-session", eventSeq: 100, at: "2026-06-05T04:00:00Z", data: { status: "started", source: "startup" } }, + { eventKind: "compact_post", sessionId: "time-primary-session", eventSeq: 1, at: "2026-06-05T05:00:00Z", data: { compactSummary: "after reset" } } + ], + session: "time-primary-session" +}); +process.stdout.write(JSON.stringify(result)); +JS +)" +assert_json_expr "etrnl compact reset boundary uses timestamp before sequence" "$etrnl_reset_time_primary_json" '.found == true and .handoff.summary == "after reset"' if ETRNL_STATE_DIR="$etrnl_state_dir" node "$ROOT/scripts/etrnl-state.mjs" stop-status --session fixture-compact --json >/dev/null 2>&1; then not_ok "etrnl stop-status blocks stale compact verification" else @@ -1798,6 +1816,11 @@ if [[ -f "$INIT_SCRIPT" ]]; then else ok "init refuses missing --profile" fi + if bash "$INIT_SCRIPT" --profile does-not-exist --dry-run "$init_target" >/dev/null 2>&1; then + not_ok "init propagates module collection errors" + else + ok "init propagates module collection errors" + fi # init --check reports stale after manifest bump real_target="$TMPROOT/init-real-target" @@ -1844,6 +1867,7 @@ else not_ok "init dry-run mentions eternal-saas" not_ok "init --dry-run does not write files" not_ok "init refuses missing --profile" + not_ok "init propagates module collection errors" not_ok "init --check reports stale after manifest bump" not_ok "init --check reports locally-modified" not_ok "init refuses to overwrite locally-modified without --force" From 4224b7428ce755b59b719cc5c4aad93b08bfab45 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 15:24:18 -0700 Subject: [PATCH 08/24] update workflow and triage review notes --- .github/workflows/health.yml | 3 ++- commands/email-triage.md | 2 +- skills/bundled/orpc-patterns/references/getting-started.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml index 7418eb5..82fb3b6 100644 --- a/.github/workflows/health.yml +++ b/.github/workflows/health.yml @@ -18,9 +18,10 @@ jobs: repo-health: name: Repository Health Checks runs-on: ubuntu-latest + timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: persist-credentials: false diff --git a/commands/email-triage.md b/commands/email-triage.md index c5d2e85..f07124a 100644 --- a/commands/email-triage.md +++ b/commands/email-triage.md @@ -30,7 +30,7 @@ If `guarded-run` exits with `TRIAGE_GUARD_ML_DISAGREED`, do not ask the reposito ```bash etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights etrnl-email triage ml-reviews --latest --account --limit 20 -etrnl-email triage report --latest --account --include-failures --format markdown +etrnl-email triage report --run-id --format markdown ``` Phase 2 starts only after Inbox Zero is verified. Use the queue run id emitted by the runtime, then show exactly one action/reply queue item: diff --git a/skills/bundled/orpc-patterns/references/getting-started.md b/skills/bundled/orpc-patterns/references/getting-started.md index 528beba..2b6ff9b 100644 --- a/skills/bundled/orpc-patterns/references/getting-started.md +++ b/skills/bundled/orpc-patterns/references/getting-started.md @@ -2,7 +2,7 @@ ## Prerequisites -- Node.js 18+ works; Node.js 20+ is default for new projects. +- Node.js 18+ works; Node.js 24+ is default for new projects. - Bun, Deno, and Cloudflare Workers are supported through adapters. - TypeScript strict mode is documented. - oRPC supports Zod, Valibot, Arktype, and any Standard Schema implementation. From 2ec364dba42ebf0c357c0382493906ff1868c3b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 15:33:22 -0700 Subject: [PATCH 09/24] document cursor rule install coverage --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f257be9..0b86c9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this ### Changed - `scripts/sync-rule-exports.mjs` and `scripts/init-project-rules.sh` — manifest-driven rule sync now validates profile membership, generated Cursor exports, privacy overlays, and install-time Cursor checksums. +- `scripts/init-project-rules.sh` — installs generated Cursor `.mdc` modules alongside Claude rules, validates Cursor exports, and tracks Cursor checksums in the install receipt. - `rules/eternal-saas/*` — rule host metadata now reflects Claude and Cursor support without claiming unsupported Codex nested context output. - `skills/bundled/stripe-best-practices` — hardens Stripe guidance from advisory wording to explicit policy gates for API versions, payment-surface selection, test/migration expectations, and Connect settlement/dispute behavior. From b7468ab0d90a87e505067c1d15d70bbb11a3f950 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 15:55:54 -0700 Subject: [PATCH 10/24] align tcg rule export scopes --- rules-manifest.json | 20 +++++++++---------- rules/eternal-saas/project/tcg-contract.md | 16 +++++++-------- .../project/eternal-saas-tcg-contract.mdc | 16 +++++++-------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/rules-manifest.json b/rules-manifest.json index bee2e7c..efe860e 100644 --- a/rules-manifest.json +++ b/rules-manifest.json @@ -464,14 +464,14 @@ "packages/**/*collectible-card*/**" ], "globs": [ - "apps/**/tcg*", - "apps/**/tcg-card*", - "apps/**/trading-card*", - "apps/**/collectible-card*", - "packages/**/tcg*", - "packages/**/tcg-card*", - "packages/**/trading-card*", - "packages/**/collectible-card*" + "apps/**/*tcg*/**", + "apps/**/*tcg-card*/**", + "apps/**/*trading-card*/**", + "apps/**/*collectible-card*/**", + "packages/**/*tcg*/**", + "packages/**/*tcg-card*/**", + "packages/**/*trading-card*/**", + "packages/**/*collectible-card*/**" ], "hosts": [ "claude", @@ -481,8 +481,8 @@ "eternal-saas-tcg" ], "verify": "pnpm test", - "checksum": "64aa62c9bec37d35493c51b4569c387ec1cbcbff5d945fa4ce7224166a8e2cf7", - "mdcChecksum": "077c3e9b44f482cfc41d4e92852e3b12981c1a89cf54f71e75a071bdae5ffba2", + "checksum": "1d18155d4a032776c0f1feafdd102cc1d71f10dd7d024ef314d45d8095fa1fbf", + "mdcChecksum": "816cc60d8e9aa707a56ac0189407c3586fabc2bb5d0655c2100bafae89332a31", "cursorPath": "templates/cursor/rules/eternal-saas/project/eternal-saas-tcg-contract.mdc", "generatedAt": "2026-06-16T18:32:06.270Z" }, diff --git a/rules/eternal-saas/project/tcg-contract.md b/rules/eternal-saas/project/tcg-contract.md index 77f0c49..75362c8 100644 --- a/rules/eternal-saas/project/tcg-contract.md +++ b/rules/eternal-saas/project/tcg-contract.md @@ -10,14 +10,14 @@ paths: - "packages/**/*trading-card*/**" - "packages/**/*collectible-card*/**" globs: - - "apps/**/tcg*" - - "apps/**/tcg-card*" - - "apps/**/trading-card*" - - "apps/**/collectible-card*" - - "packages/**/tcg*" - - "packages/**/tcg-card*" - - "packages/**/trading-card*" - - "packages/**/collectible-card*" + - "apps/**/*tcg*/**" + - "apps/**/*tcg-card*/**" + - "apps/**/*trading-card*/**" + - "apps/**/*collectible-card*/**" + - "packages/**/*tcg*/**" + - "packages/**/*tcg-card*/**" + - "packages/**/*trading-card*/**" + - "packages/**/*collectible-card*/**" description: "TCG rules: collectible-card domain contracts, inventory safety, and market-data verification." hosts: [claude, cursor] alwaysApply: false diff --git a/templates/cursor/rules/eternal-saas/project/eternal-saas-tcg-contract.mdc b/templates/cursor/rules/eternal-saas/project/eternal-saas-tcg-contract.mdc index 0e27551..f9f279b 100644 --- a/templates/cursor/rules/eternal-saas/project/eternal-saas-tcg-contract.mdc +++ b/templates/cursor/rules/eternal-saas/project/eternal-saas-tcg-contract.mdc @@ -1,13 +1,13 @@ --- globs: - - "apps/**/tcg*" - - "apps/**/tcg-card*" - - "apps/**/trading-card*" - - "apps/**/collectible-card*" - - "packages/**/tcg*" - - "packages/**/tcg-card*" - - "packages/**/trading-card*" - - "packages/**/collectible-card*" + - "apps/**/*tcg*/**" + - "apps/**/*tcg-card*/**" + - "apps/**/*trading-card*/**" + - "apps/**/*collectible-card*/**" + - "packages/**/*tcg*/**" + - "packages/**/*tcg-card*/**" + - "packages/**/*trading-card*/**" + - "packages/**/*collectible-card*/**" alwaysApply: false description: "TCG rules: collectible-card domain contracts, inventory safety, and market-data verification." --- From 1e1a80fcc91cbee9314494eb62d41f708c4ddb8d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 16:20:42 -0700 Subject: [PATCH 11/24] document privacy overlay format --- rules-manifest.json | 1 + .../bundled/orpc-patterns/references/migrations-cookbook.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rules-manifest.json b/rules-manifest.json index efe860e..069cff5 100644 --- a/rules-manifest.json +++ b/rules-manifest.json @@ -19,6 +19,7 @@ ".eternal/privacy-banned-tokens.local", "rules-manifest.local.json" ], + "localTokenFilesFormat": ".eternal/privacy-banned-tokens.local is a newline-delimited token list with blank lines and # comments ignored. rules-manifest.local.json may be a JSON array, an object with privacy.bannedTokens, or an object with bannedTokens. Local tokens are merged additively with bannedTokens by scripts/sync-rule-exports.mjs and scripts/doctor.sh; they do not override tracked manifest fields.", "policy": "sync-rule-exports.mjs --check fails when any tracked rule file contains a banned token. Tracked tokens are generic placeholders only. Client repo names, account facts, credentials, and personal identity must stay in local gitignored overlays." }, "profiles": { diff --git a/skills/bundled/orpc-patterns/references/migrations-cookbook.md b/skills/bundled/orpc-patterns/references/migrations-cookbook.md index c147aab..10258ba 100644 --- a/skills/bundled/orpc-patterns/references/migrations-cookbook.md +++ b/skills/bundled/orpc-patterns/references/migrations-cookbook.md @@ -18,7 +18,7 @@ - Keep plain Server Actions for small UI-local mutations. - Move to oRPC when the mutation needs typed reuse, public API shape, OpenAPI, tests against a stable contract, or AI-agent-readable schemas. -- Use `.actionable()` when one procedure must support both paths. +- Use `.actionable()` when one procedure supports both paths. ## Form Validation @@ -46,7 +46,7 @@ return data; ## Common Review Nits - Missing `.output(...)` on public procedures. -- `OpenAPIHandler` used where `RPCHandler` must serve normal clients. +- Use `RPCHandler` for normal clients. Use `OpenAPIHandler` when OpenAPI requirements define the surface. - GET client calls without procedure `route({ method: "GET" })`. - Contract-first implementation missing `.router(...)`. - Minified contract used with request/response validation plugins. From e8f1bf8ffd54d39f1c316031d47ef3ac769a761c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 16:39:27 -0700 Subject: [PATCH 12/24] cover workflow review follow-ups --- .github/workflows/health.yml | 1 + CHANGELOG.md | 2 ++ tests/test-workflow-tools.sh | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml index 82fb3b6..c5885f7 100644 --- a/.github/workflows/health.yml +++ b/.github/workflows/health.yml @@ -29,6 +29,7 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: "22" + # No package install runs in this job; keep setup-node package-manager caching disabled. cache: "" - name: Install CLI dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b86c9e..1ed4c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this - `scripts/sync-rule-exports.mjs` and `scripts/init-project-rules.sh` — manifest-driven rule sync now validates profile membership, generated Cursor exports, privacy overlays, and install-time Cursor checksums. - `scripts/init-project-rules.sh` — installs generated Cursor `.mdc` modules alongside Claude rules, validates Cursor exports, and tracks Cursor checksums in the install receipt. - `rules/eternal-saas/*` — rule host metadata now reflects Claude and Cursor support without claiming unsupported Codex nested context output. +- Email triage runtime references now use the `etrnl-email` command pattern across guards, canaries, slash commands, and fixtures instead of legacy `vivaz-email` naming. +- Bundled skill namespaces now align around `@example-suite`, `money-vo-discipline`, and `orpc-patterns` naming across policy skills, routing lists, and vendored bundles. - `skills/bundled/stripe-best-practices` — hardens Stripe guidance from advisory wording to explicit policy gates for API versions, payment-surface selection, test/migration expectations, and Connect settlement/dispute behavior. ### Fixed diff --git a/tests/test-workflow-tools.sh b/tests/test-workflow-tools.sh index 77b2aca..550f237 100755 --- a/tests/test-workflow-tools.sh +++ b/tests/test-workflow-tools.sh @@ -492,6 +492,11 @@ if perf_invalid_json="$(printf '{' | node "$ROOT/scripts/performance-baseline.mj else assert_contains "performance baseline reports invalid JSON" "$perf_invalid_json" "invalid JSON from stdin" fi +if perf_empty_stdin="$(printf '' | node "$ROOT/scripts/performance-baseline.mjs" create 2>&1)"; then + not_ok "performance baseline rejects empty stdin" +else + assert_contains "performance baseline rejects empty stdin" "$perf_empty_stdin" "stdin closed without JSON; pipe JSON and close stdin/EOF" +fi if perf_stdin_timeout="$(ETRNL_STDIN_TIMEOUT_MS=1 node "$ROOT/scripts/performance-baseline.mjs" create < <(sleep 0.05) 2>&1)"; then not_ok "performance baseline fails when stdin does not close" else From 3ee5fca12ca9b024f942a1f2425deec626875e00 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 17:04:12 -0700 Subject: [PATCH 13/24] harden privacy and ci review notes --- docs/configuration.md | 1 + scripts/doctor.sh | 5 +++ scripts/init-project-rules.sh | 6 +++- scripts/sync-rule-exports.mjs | 31 ++++++++++++++++--- .../github-actions/docker-release.yml | 7 +++-- 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0819ad7..cbd3f8b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -24,6 +24,7 @@ Install: - `local-daemon` mode requires a local Hindsight daemon or `uvx hindsight-embed`/`hindsight-embed`; set `HINDSIGHT_DAEMON_SOCKET` only when your local daemon uses a non-default socket. - `HINDSIGHT_API_URL` is required for `external-api` mode; `HINDSIGHT_API_TOKEN` remains an environment secret and is not written to tracked files. - `docker-server` mode requires Docker plus the Hindsight image selection, such as `HINDSIGHT_DOCKER_IMAGE` and `HINDSIGHT_DOCKER_TAG`; configure registry credentials and host port mapping outside tracked files. +- `ETRNL_CODEGRAPH_NPM_SPEC` and `ETRNL_BEADS_NPM_SPEC` override admin-tool npm install specs for full-profile bootstrap. Set them only from trusted administrator-controlled environments; unsanitized input enables arbitrary local command execution through shell interpolation in npm install commands. Updater: diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 1c3de92..1f5cd44 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -849,6 +849,11 @@ root = Path(sys.argv[1]) manifest = json.loads((root / "rules-manifest.json").read_text()) tokens = [str(token).lower() for token in manifest.get("privacy", {}).get("bannedTokens", []) if str(token).strip()] local_token_files = {str(rel_path) for rel_path in manifest.get("privacy", {}).get("localTokenFiles", [])} +for rel_path in sorted(local_token_files): + check = subprocess.run(["git", "-C", str(root), "check-ignore", "--quiet", "--", rel_path]) + if check.returncode != 0: + print(f"local privacy token file is not gitignored: {rel_path}", file=sys.stderr) + sys.exit(1) for rel_path in manifest.get("privacy", {}).get("localTokenFiles", []): local_path = root / str(rel_path) if not local_path.exists(): diff --git a/scripts/init-project-rules.sh b/scripts/init-project-rules.sh index b8bc2d9..e483d62 100755 --- a/scripts/init-project-rules.sh +++ b/scripts/init-project-rules.sh @@ -49,7 +49,11 @@ CURSOR_RULES_DEST="$TARGET/.cursor/rules/eternal-saas" MANIFEST_RECEIPT="$CLAUDE_RULES_DEST/.manifest.json" file_sha256() { - shasum -a 256 "$1" 2>/dev/null | awk '{print $1}' || sha256sum "$1" | awk '{print $1}' + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + sha256sum "$1" | awk '{print $1}' + fi } receipt_checksum_for() { diff --git a/scripts/sync-rule-exports.mjs b/scripts/sync-rule-exports.mjs index 7fcae4f..4faf589 100755 --- a/scripts/sync-rule-exports.mjs +++ b/scripts/sync-rule-exports.mjs @@ -137,7 +137,12 @@ function localPrivacyTokens(filePath) { if (!existsSync(filePath)) return []; const content = readFileSync(filePath, 'utf8'); if (filePath.endsWith('.json')) { - const parsed = JSON.parse(content); + let parsed; + try { + parsed = JSON.parse(content); + } catch (error) { + throw new Error(`malformed local privacy JSON in ${filePath}: ${error.message}`); + } if (Array.isArray(parsed)) return parsed; return arrayValue(parsed.privacy?.bannedTokens || parsed.bannedTokens); } @@ -166,9 +171,21 @@ function moduleSourcePath(key, rulesRoot) { function walkMd(dir) { const files = []; if (!existsSync(dir)) return files; - for (const entry of readdirSync(dir)) { + let entries; + try { + entries = readdirSync(dir); + } catch (error) { + throw new Error(`cannot read rule directory ${dir}: ${error.message}`); + } + for (const entry of entries) { const full = join(dir, entry); - if (statSync(full).isDirectory()) files.push(...walkMd(full)); + let stat; + try { + stat = statSync(full); + } catch (error) { + throw new Error(`cannot stat rule path ${full}: ${error.message}`); + } + if (stat.isDirectory()) files.push(...walkMd(full)); else if (entry.endsWith('.md')) files.push(full); } return files.sort((left, right) => left.localeCompare(right)); @@ -292,7 +309,13 @@ if (singleSource) { const rulesRoot = join(ROOT, 'rules', 'eternal-saas'); const cursorOutputRoot = join(ROOT, 'templates', 'cursor', 'rules', 'eternal-saas'); const expectedKeys = expectedModuleKeys(manifest); - const moduleFiles = walkMd(rulesRoot); + let moduleFiles = []; + try { + moduleFiles = walkMd(rulesRoot); + } catch (err) { + errors.push(err.message); + exitCode = 1; + } const fileKeys = new Set(moduleFiles.map((filePath) => sourceRelKey(filePath, rulesRoot))); for (const key of expectedKeys) { diff --git a/skills/bundled/ci-cd/templates/github-actions/docker-release.yml b/skills/bundled/ci-cd/templates/github-actions/docker-release.yml index 16bf61f..d5d7aed 100644 --- a/skills/bundled/ci-cd/templates/github-actions/docker-release.yml +++ b/skills/bundled/ci-cd/templates/github-actions/docker-release.yml @@ -1,5 +1,8 @@ name: Build and Publish Image +# Production note: Trivy blocks HIGH/CRITICAL findings by default. Use the +# documented non-blocking escape hatch only with a tracked baseline and owners. + on: push: branches: [main] @@ -60,5 +63,5 @@ jobs: with: image-ref: ${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} severity: HIGH,CRITICAL - # Reference-only until a baseline artifact or tracked issue lists confirmed exploitable findings with owners, dispositions, and target dates; then switch to exit-code: "1". - exit-code: "0" + # Legacy-image escape hatch only: switch to "0" after recording a baseline artifact or tracked issue for each finding. + exit-code: "1" From aef2d52757260e8032942f310557b8e07c2a0e21 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 17:31:32 -0700 Subject: [PATCH 14/24] resolve coderabbit guard follow-ups --- hooks/cc-pretooluse-guard.sh | 8 ++++---- hooks/cc-stop-verifier.sh | 4 ++-- hooks/cc-userprompt-router.sh | 2 +- scripts/sync-rule-exports.mjs | 3 +++ skills/bundled/finding-duplicate-functions/SKILL.md | 2 ++ tests/test-hooks.sh | 3 ++- tests/test-workflow-tools.sh | 1 + 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/hooks/cc-pretooluse-guard.sh b/hooks/cc-pretooluse-guard.sh index 41e0871..7f0d8cb 100755 --- a/hooks/cc-pretooluse-guard.sh +++ b/hooks/cc-pretooluse-guard.sh @@ -672,19 +672,19 @@ handle_bash() { fi if cc_email_triage_active && command_is_raw_email_triage_gmail_mutation "$cmd"; then - deny "Raw Gmail mutation is blocked during email-triage. Phase 1 must use etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Only after verified Inbox Zero, open the queue." + deny "Raw Gmail mutation is blocked during email-triage. Phase 1 must use etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Only after verified Inbox Zero, open the queue." fi if cc_email_triage_active && command_is_email_triage_dry_run "$cmd" && ! command_is_email_triage_debug_dry_run "$cmd"; then - deny "Dry email-triage runs are blocked during /email-triage. Phase 1 must clear INBOX with etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account before any queue item is shown." + deny "Dry email-triage runs are blocked during /email-triage. Phase 1 must clear INBOX with etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account before any queue item is shown." fi if cc_email_triage_active && command_is_email_triage_queue "$cmd" && ! cc_email_triage_verify_seen; then - deny "email-triage queue is blocked until Inbox Zero verification has run. First run etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Open the queue only after verify reports inbox_zero_verified true and inbox_count 0." + deny "email-triage queue is blocked until Inbox Zero verification has run. First run etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Open the queue only after verify reports inbox_zero_verified true and inbox_count 0." fi if cc_email_triage_active && command_is_email_triage_queue "$cmd" && ! cc_email_triage_queue_verified "$cmd"; then - deny "email-triage queue is blocked until provider verification proves Inbox Zero and either gmail_mutated true or queue_ready_without_mutation true. Run etrnl-email triage verify --latest --account and require inbox_zero_verified true and inbox_count 0 before opening the queue." + deny "email-triage queue is blocked until provider verification proves Inbox Zero and either gmail_mutated true or queue_ready_without_mutation true. Run etrnl-email triage verify --latest --account and require inbox_zero_verified true and inbox_count 0 before opening the queue." fi if command_writes_live_claude_hooks "$cmd"; then diff --git a/hooks/cc-stop-verifier.sh b/hooks/cc-stop-verifier.sh index 701006b..575eb6b 100755 --- a/hooks/cc-stop-verifier.sh +++ b/hooks/cc-stop-verifier.sh @@ -376,7 +376,7 @@ if [[ "$claims_done" == "true" ]]; then if cc_email_triage_requested; then email_triage_since="$(cc_email_triage_request_at)" if ! cc_email_triage_evidence_after "$email_triage_since"; then - cc_json_block "email-triage phase 1 must clear INBOX first. Run etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights and verify Inbox Zero before opening the action queue." + cc_json_block "email-triage phase 1 must clear INBOX first. Run etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights and verify Inbox Zero before opening the action queue." exit 0 fi if ! cc_email_triage_verify_latest "$email_triage_since"; then @@ -384,7 +384,7 @@ if [[ "$claims_done" == "true" ]]; then exit 0 fi if ! cc_email_triage_verify_applied "$email_triage_since"; then - cc_json_block "email-triage Inbox Zero completion requires provider-verified INBOX zero and either gmail_mutated true or queue_ready_without_mutation true. Use etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, verify inbox_count is 0, then open the action queue." + cc_json_block "email-triage Inbox Zero completion requires provider-verified INBOX zero and either gmail_mutated true or queue_ready_without_mutation true. Use etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, verify inbox_count is 0, then open the action queue." exit 0 fi if ! cc_email_triage_message_has_runtime_output; then diff --git a/hooks/cc-userprompt-router.sh b/hooks/cc-userprompt-router.sh index 07e304a..c19905a 100755 --- a/hooks/cc-userprompt-router.sh +++ b/hooks/cc-userprompt-router.sh @@ -334,7 +334,7 @@ if [[ "$prompt_lower" =~ /email-triage|email[[:space:]-]+triage ]]; then if [[ -n "$email_triage_account" ]]; then notes+=("Use /email-triage as two phases. Phase 1: Inbox Zero first. Run: etrnl-email triage guarded-run --account $email_triage_account --max-inbox 500 --apply --require-insights, then verify with etrnl-email triage verify --latest --account $email_triage_account. Do not open the queue unless verify reports inbox_zero_verified true, inbox_count 0, and either gmail_mutated true or queue_ready_without_mutation true. Phase 2: only after Inbox Zero, paste one generated queue item with etrnl-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") else - notes+=("Use /email-triage as two phases. If no account id is present, ask for it. Phase 1: Inbox Zero first with etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Do not open the queue unless verify reports inbox_zero_verified true, inbox_count 0, and either gmail_mutated true or queue_ready_without_mutation true. Phase 2: only after Inbox Zero, paste one generated queue item with etrnl-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") + notes+=("Use /email-triage as two phases. If no account id is present, ask for it. Phase 1: Inbox Zero first with etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights, then etrnl-email triage verify --latest --account . Do not open the queue unless verify reports inbox_zero_verified true, inbox_count 0, and either gmail_mutated true or queue_ready_without_mutation true. Phase 2: only after Inbox Zero, paste one generated queue item with etrnl-email triage queue --run-id --mode reply --format markdown --next. Do not say triage complete while an item is active.") fi fi if [[ "$prompt_lower" =~ disk[[:space:]-]+cleanup|clean[[:space:]]+up[[:space:]]+disk|free[[:space:]]+(disk|ssd|storage)[[:space:]]+space|reclaim[[:space:]]+(disk|ssd|storage)[[:space:]]+space ]]; then diff --git a/scripts/sync-rule-exports.mjs b/scripts/sync-rule-exports.mjs index 4faf589..2be0575 100755 --- a/scripts/sync-rule-exports.mjs +++ b/scripts/sync-rule-exports.mjs @@ -196,6 +196,9 @@ function expandProfileModules(manifest, profileName, seen = new Set()) { const profile = manifest.profiles?.[profileName]; if (!profile) throw new Error(`Unknown profile: ${profileName}`); seen.add(profileName); + if (profile.extends && typeof profile.extends !== 'string') { + throw new Error(`Profile ${profileName}: extends must be a single profile name string`); + } const inherited = profile.extends ? expandProfileModules(manifest, profile.extends, seen) : []; return [...inherited, ...arrayValue(profile.modules)]; } diff --git a/skills/bundled/finding-duplicate-functions/SKILL.md b/skills/bundled/finding-duplicate-functions/SKILL.md index 2350809..b889299 100644 --- a/skills/bundled/finding-duplicate-functions/SKILL.md +++ b/skills/bundled/finding-duplicate-functions/SKILL.md @@ -92,6 +92,8 @@ Save each output as `./duplicates/{category}.json`. Produces a prioritized markdown report grouped by confidence level. +Each duplicate entry includes `rules.action`, `rules.reason`, and `rules.survivor` when a survivor exists; these fields become the report action text and survivor annotation. See `scripts/find-duplicates-prompt.md` for the full duplicate JSON structure. + ### Phase 6: Human Review Review the report. For HIGH confidence duplicates: diff --git a/tests/test-hooks.sh b/tests/test-hooks.sh index 6b46afa..e051817 100755 --- a/tests/test-hooks.sh +++ b/tests/test-hooks.sh @@ -663,6 +663,7 @@ assert_contains "stop verifier blocks outstanding browser QA" "$out" "Outstandin paused_prod_state="$TMPROOT/claude-guard-fixture-paused-prod-status.json" jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],activePlanPath:"",activePlanPathUpdatedAt:"",planExecutionRequested:false,planExecutionRequestedAt:"",lastPrompt:"did u read the handoff file?",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$paused_prod_state" +# Keep this transcript anonymized; the test only needs a paused production deploy shape. paused_prod_message=$'Yes. It was injected as the restored handoff.\n\n1. Check PR #53 CI - green\n2. Merge - done\n3. Deploy to prod example-admin - was watching GHCR build-and-push, in_progress\n4. Set bruno to master in prod DB - only AFTER deploy\n\nBefore I SSH into prod: do you want me to proceed with the deploy once the GHCR build is green?\nNothing is live yet. Awaiting your answer before I SSH to prod.' paused_prod_stop="$(jq -cn --arg message "$paused_prod_message" '{session_id:"fixture-paused-prod-status",last_assistant_message:$message,stop_hook_active:false}')" out="$(run_hook cc-stop-verifier.sh "$paused_prod_stop")" @@ -714,7 +715,7 @@ email_triage_missing_state="$TMPROOT/claude-guard-fixture-email-triage-missing.j jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage fixture-account",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_missing_state" email_triage_missing_stop="$(jq -cn '{session_id:"fixture-email-triage-missing",last_assistant_message:"Done, email triage complete.",stop_hook_active:false}')" out="$(PATH="$TMPROOT/bin:$PATH" run_hook cc-stop-verifier.sh "$email_triage_missing_stop")" -assert_contains "email triage stop requires runtime apply command" "$out" "etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights" +assert_contains "email triage stop requires runtime apply command" "$out" "etrnl-email triage guarded-run --account --max-inbox 500 --apply --require-insights" email_triage_ok_state="$TMPROOT/claude-guard-fixture-email-triage-ok.json" jq -nc '{schemaVersion:4,reads:{},searches:{},edits:{},commands:[],blockedCommands:[],successfulCommands:[{command:"etrnl-email triage guarded-run --account fixture-account --max-inbox 500 --apply --require-insights",at:"2026-01-01T00:00:01Z"}],failures:[],skillCalls:[],agentCalls:[],reviewerAgentCalls:[],requestedSkills:[{value:"email-triage",at:"2026-01-01T00:00:00Z"}],evidenceChallenges:[],evidenceDisciplineViolations:[],evidenceViolationFingerprints:{},warningFingerprints:{},verificationRuns:[],qualityRuns:[],testRuns:[],browserRuns:[],reviewRuns:[],newFileSearches:[],newSourceFiles:{},editCounts:{},largeEdits:[],repeatedEditFiles:{},reviewTriggers:[],editGeneration:0,commandLastEditGeneration:{},prodApprovalMarkers:[],lastPrompt:"/email-triage fixture-account",lastCompactSummary:"",lastCompactAt:"",compactCount:0,cwd:"",settingsFingerprint:"",startedAt:"2026-01-01T00:00:00Z"}' >"$email_triage_ok_state" diff --git a/tests/test-workflow-tools.sh b/tests/test-workflow-tools.sh index 550f237..d19e0b3 100755 --- a/tests/test-workflow-tools.sh +++ b/tests/test-workflow-tools.sh @@ -1841,6 +1841,7 @@ if [[ -f "$INIT_SCRIPT" ]]; then orpc_source="$ROOT/rules/eternal-saas/project/orpc.md" orpc_mtime_marker="$TMPROOT/orpc-source-mtime" if [[ -f "$orpc_source" ]]; then + touch "$orpc_mtime_marker" touch -r "$orpc_source" "$orpc_mtime_marker" trap 'touch -r "$orpc_mtime_marker" "$orpc_source" 2>/dev/null || true; cc_test_cleanup' EXIT sleep 1 From 846a866f2095f553b8baf945cbd0bacf854e101c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 18:01:14 -0700 Subject: [PATCH 15/24] address workflow metadata review notes --- .github/workflows/health.yml | 2 +- .gitignore | 4 ++-- rules-manifest.json | 2 ++ scripts/sync-rule-exports.mjs | 11 +++++++++++ tests/test-workflow-tools.sh | 21 +++++++++++++++++++++ 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml index c5885f7..1e2f52d 100644 --- a/.github/workflows/health.yml +++ b/.github/workflows/health.yml @@ -30,7 +30,7 @@ jobs: with: node-version: "22" # No package install runs in this job; keep setup-node package-manager caching disabled. - cache: "" + package-manager-cache: false - name: Install CLI dependencies run: | diff --git a/.gitignore b/.gitignore index 124e151..53784ef 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,8 @@ plans/ docs/plans/ docs/research/ # Local privacy overlay is intentionally ignored without hiding all .eternal/ state. -.eternal/privacy-banned-tokens.local -rules-manifest.local.json +/.eternal/privacy-banned-tokens.local +/rules-manifest.local.json # Never commit private Claude runtime data. settings.local.json diff --git a/rules-manifest.json b/rules-manifest.json index 069cff5..f7fde37 100644 --- a/rules-manifest.json +++ b/rules-manifest.json @@ -65,6 +65,8 @@ "profile": "Profile or array of profiles this module belongs to.", "verify": "Shell command to verify the rule is effective in the target repo.", "checksum": "SHA-256 of the canonical module source file (populated by sync).", + "mdcChecksum": "SHA-256 of the generated Cursor .mdc template file (populated by sync).", + "cursorPath": "Relative path to the generated Cursor .mdc template file.", "generatedAt": "ISO timestamp of last sync run." } }, diff --git a/scripts/sync-rule-exports.mjs b/scripts/sync-rule-exports.mjs index 2be0575..b1fabdb 100755 --- a/scripts/sync-rule-exports.mjs +++ b/scripts/sync-rule-exports.mjs @@ -40,7 +40,18 @@ function parseInlineArray(value) { const items = []; let current = ''; let quote = ''; + let escaped = false; for (const char of value) { + if (escaped) { + current += char; + escaped = false; + continue; + } + if (char === '\\' && quote) { + current += char; + escaped = true; + continue; + } if ((char === '"' || char === "'") && !quote) { quote = char; current += char; diff --git a/tests/test-workflow-tools.sh b/tests/test-workflow-tools.sh index d19e0b3..526171d 100755 --- a/tests/test-workflow-tools.sh +++ b/tests/test-workflow-tools.sh @@ -1753,6 +1753,27 @@ if [[ -f "$SYNC_SCRIPT" ]]; then not_ok "sync --check passes on stable output (idempotent)" fi + escaped_quote_module="$TMPROOT/escaped-quote-fixture.md" + cat >"$escaped_quote_module" <<'MD' +--- +id: escaped-quote-fixture +paths: ["src/\"quoted\"/**/*.ts"] +globs: ["src/\"quoted\"/**/*.ts"] +description: "Escaped quote fixture." +hosts: [claude, cursor] +verify: "pnpm test" +--- + +# Escaped Quote Fixture +MD + escaped_quote_out="$TMPROOT/sync-escaped-quote" + mkdir -p "$escaped_quote_out" + if node "$SYNC_SCRIPT" --source "$escaped_quote_module" --manifest "$FIXTURE_MANIFEST" --output "$escaped_quote_out" 2>/dev/null; then + assert_contains "sync preserves escaped quotes in inline arrays" "$(cat "$escaped_quote_out/escaped-quote-fixture.mdc")" 'src/\"quoted\"/**/*.ts' + else + not_ok "sync preserves escaped quotes in inline arrays" + fi + # sync --check fails on drift (mutated emitted file) mdc_drift="$sync_out_dir2/test-fixture-module.mdc" if [[ -f "$mdc_drift" ]]; then From 36942c69c805dc184cf4d90b7fce406383ac57c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 18:22:10 -0700 Subject: [PATCH 16/24] fail closed on rule copy errors --- scripts/init-project-rules.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/init-project-rules.sh b/scripts/init-project-rules.sh index e483d62..9dfa985 100755 --- a/scripts/init-project-rules.sh +++ b/scripts/init-project-rules.sh @@ -280,8 +280,8 @@ while IFS=$'\t' read -r key id; do dest_cursor="$CURSOR_RULES_DEST/$cursor_rel" mkdir -p "$(dirname "$dest_claude")" "$(dirname "$dest_cursor")" - cp "$src" "$dest_claude" - cp "$cursor_src" "$dest_cursor" + cp "$src" "$dest_claude" || { echo "error: failed to copy $rel" >&2; exit 1; } + cp "$cursor_src" "$dest_cursor" || { echo "error: failed to copy $cursor_rel" >&2; exit 1; } sum="$(file_sha256 "$dest_claude")" cursor_sum="$(file_sha256 "$dest_cursor")" From 7212672d08d5798ae2600fa1ea6fc3cba6551579 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 19:09:19 -0700 Subject: [PATCH 17/24] harden privacy scanner review notes --- .github/workflows/health.yml | 5 +- CREDITS.md | 4 +- docs/eternal-stack-coverage.md | 4 +- scripts/bootstrap-tools.sh | 2 + scripts/doctor.sh | 61 +-------------- scripts/lib/skill-lists.sh | 2 + scripts/privacy-banned-token-check.mjs | 103 +++++++++++++++++++++++++ tests/test-workflow-tools.sh | 78 ++++++++++++++++++- 8 files changed, 193 insertions(+), 66 deletions(-) create mode 100644 scripts/privacy-banned-token-check.mjs diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml index 1e2f52d..7f54095 100644 --- a/.github/workflows/health.yml +++ b/.github/workflows/health.yml @@ -17,7 +17,7 @@ concurrency: jobs: repo-health: name: Repository Health Checks - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - name: Checkout @@ -35,8 +35,9 @@ jobs: - name: Install CLI dependencies run: | sudo apt-get update - sudo apt-get install -y jq fd-find ripgrep shellcheck python3 + sudo apt-get install -y jq fd-find ripgrep shellcheck python3.12 sudo ln -sf "$(command -v fdfind)" /usr/local/bin/fd + sudo ln -sf "$(command -v python3.12)" /usr/local/bin/python3 - name: Validate generated rule exports run: node scripts/sync-rule-exports.mjs --check diff --git a/CREDITS.md b/CREDITS.md index 116e3bf..84d839b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -42,7 +42,7 @@ Eternal Stack reimplements useful mechanisms from public agent-tooling projects | --- | --- | | [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph) | Optional local code-graph MCP; bootstrap and health checks in `full` profile | | [hyhmrright/brooks-lint](https://github.com/hyhmrright/brooks-lint) | Review finding shape (Symptom → Source → Consequence → Remedy); vendored Brooks modules | -| [rtk-ai/rtk](https://github.com/rtk-ai/rtk) | Codex deterministic command rewrite via `codex-rtk-pre-tool-use.sh` | +| [rtk-ai/rtk](https://github.com/rtk-ai/rtk) | Optional Codex command rewrite helper via `codex-rtk-pre-tool-use.sh`; separate from the Claude/Cursor rule pack | | [GitHub/spec-kit](https://github.com/github/spec-kit) | Plan/readiness executability gates | | [Chachamaru127/claude-code-harness](https://github.com/Chachamaru127/claude-code-harness) | Quality gates, browser artifact contracts, review plateau ideas | | [infinri/Writ](https://github.com/infinri/Writ) | Mandatory-rule mechanical enforcement | @@ -51,7 +51,7 @@ Eternal Stack reimplements useful mechanisms from public agent-tooling projects ## Claude Code -Eternal Stack targets [Claude Code](https://docs.anthropic.com/en/docs/claude-code) hook and skill surfaces. Codex parity helpers install under `~/.codex/etrnl/` when you use both hosts. +Eternal Stack targets [Claude Code](https://docs.anthropic.com/en/docs/claude-code) hook and skill surfaces. The reusable rule pack exports to Claude and Cursor; optional Codex helper state installs under `~/.codex/etrnl/` when you use both hosts. ## Contributing upstream diff --git a/docs/eternal-stack-coverage.md b/docs/eternal-stack-coverage.md index 929b7e7..cc198ed 100644 --- a/docs/eternal-stack-coverage.md +++ b/docs/eternal-stack-coverage.md @@ -15,8 +15,8 @@ Status key: `done` means implemented in this repo; `live-gated` means intentiona | Durable artifacts | done: deep-stack bundle, sanitized source manifest, skill matrix, review phase records, TDD evidence, reuse inventory and reuse bindings, findings ledger, completion audit and completion reconciliation, TypeScript trigger evidence, install proof, Hybrid risk tier, review log, browser QA v1/v2 reports with mandatory console/network summaries, route/viewport matrix counts, screenshot hashes, capture freshness, and provenance for completed v2 runs, context save/restore, artifact-required ledger checks, redacted cross-session project buglog hints, workflow-health summaries/status JSON, and local tool-effectiveness verdict summaries. | | Agent templates | done: default-installed executor/reviewer/investigator/scout/adversary/design/DX/browser QA `etrnl-*` agents. | | Shared startup guidance | done: public `AGENTS.md` template plus tiny Claude wrapper importing it. | -| Rules | done: namespaced `rules/etrnl/*.md` to avoid clobbering existing user rules; cross-host eternal-saas pack (`rules/eternal-saas/global/` + `rules/eternal-saas/project/`, including `eternal-saas-tcg` and `project/tcg-contract`) with `rules-manifest.json` (schema v1, checksums, privacy gates); `scripts/init-project-rules.sh` deploys pack to any target repo with Claude+Cursor outputs; `scripts/sync-rule-exports.mjs` keeps `.mdc` Cursor twins in sync; doctor byte gate prevents Codex startup overflow. Reference: `docs/rules.md`. | -| Install/update/rollback/doctor | done: scripts, tests, rollback (including `rules/eternal-saas` + Codex startup file restore), canaries, rules, docs, templates, agents, settings audit/repair, installed update metadata, drift explain output, artifact helpers, post-upgrade browser-QA canary, installed-home doctor, and workflow-health helpers are installed or checked. | +| Rules | done: namespaced `rules/etrnl/*.md` to avoid clobbering existing user rules; cross-host eternal-saas pack (`rules/eternal-saas/global/` + `rules/eternal-saas/project/`, including `eternal-saas-tcg` and `project/tcg-contract`) with `rules-manifest.json` (schema v1, checksums, privacy gates); `scripts/init-project-rules.sh` deploys pack to any target repo with Claude+Cursor outputs; `scripts/sync-rule-exports.mjs` keeps `.mdc` Cursor twins in sync; doctor byte gate prevents startup guidance overflow. Reference: `docs/rules.md`. | +| Install/update/rollback/doctor | done: scripts, tests, rollback (including `rules/eternal-saas` plus startup file restore), canaries, rules, docs, templates, agents, settings audit/repair, installed update metadata, drift explain output, artifact helpers, post-upgrade browser-QA canary, installed-home doctor, and workflow-health helpers are installed or checked. | | Hindsight memory consolidation | live-gated: canary verifies strict config; actual migration/removal of competing memory systems remains a personal live operation. | | Plugin/MCP cleanup | live-gated: plan requires inventory and explicit local rollout before removing plugins, MCPs, or permissions. | | Shareable repo boundary | done: public templates exclude private identity, accounts, transcripts, secrets, and memories. | diff --git a/scripts/bootstrap-tools.sh b/scripts/bootstrap-tools.sh index 7859e67..ab4ae83 100755 --- a/scripts/bootstrap-tools.sh +++ b/scripts/bootstrap-tools.sh @@ -12,6 +12,8 @@ SKIP_CODEGRAPH=0 SKIP_BEADS=0 SKIP_HINDSIGHT=0 DRY_RUN=0 +# Admin-tool npm specs: set ETRNL_*_NPM_SPEC only from trusted administrator-controlled input. +# Unsanitized values enable arbitrary command execution through shell interpolation. CODEGRAPH_NPM_SPEC="${ETRNL_CODEGRAPH_NPM_SPEC:-@colbymchenry/codegraph@1.0.1}" BEADS_NPM_SPEC="${ETRNL_BEADS_NPM_SPEC:-@beads/bd@1.0.5}" CONFIRM_SKIPPED=64 diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 1f5cd44..9b96de0 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -839,63 +839,10 @@ if git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1 && [[ -f "$ROO if [[ -f "$ROOT/rules-manifest.json" ]]; then if ! command -v python3 >/dev/null 2>&1; then fail "python3 unavailable; privacy banned-token scan cannot run" - elif privacy_out="$(python3 - "$ROOT" <<'PYEOF' 2>&1 -import json -import subprocess -import sys -from pathlib import Path - -root = Path(sys.argv[1]) -manifest = json.loads((root / "rules-manifest.json").read_text()) -tokens = [str(token).lower() for token in manifest.get("privacy", {}).get("bannedTokens", []) if str(token).strip()] -local_token_files = {str(rel_path) for rel_path in manifest.get("privacy", {}).get("localTokenFiles", [])} -for rel_path in sorted(local_token_files): - check = subprocess.run(["git", "-C", str(root), "check-ignore", "--quiet", "--", rel_path]) - if check.returncode != 0: - print(f"local privacy token file is not gitignored: {rel_path}", file=sys.stderr) - sys.exit(1) -for rel_path in manifest.get("privacy", {}).get("localTokenFiles", []): - local_path = root / str(rel_path) - if not local_path.exists(): - continue - if local_path.suffix == ".json": - try: - parsed = json.loads(local_path.read_text()) - except json.JSONDecodeError as error: - print(f"malformed JSON in {rel_path}: {error}", file=sys.stderr) - sys.exit(1) - if isinstance(parsed, list): - local_tokens = parsed - else: - local_tokens = parsed.get("privacy", {}).get("bannedTokens", parsed.get("bannedTokens", [])) - else: - local_tokens = [ - line.strip() - for line in local_path.read_text().splitlines() - if line.strip() and not line.strip().startswith("#") - ] - tokens.extend(str(token).lower() for token in local_tokens if str(token).strip()) -tokens = sorted(set(tokens)) -files = subprocess.check_output(["git", "-C", str(root), "ls-files"], text=True).splitlines() -violations = [] -for rel in files: - if rel == "rules-manifest.json" or rel in local_token_files: - continue - path = root / rel - try: - if path.stat().st_size > 10 * 1024 * 1024: - continue - text = path.read_text(errors="ignore").lower() - except OSError: - continue - found_count = sum(1 for token in tokens if token in text) - if found_count: - violations.append(f"{rel}: banned token match count={found_count}") -if violations: - print("\n".join(violations)) - sys.exit(1) -PYEOF - )"; then + elif privacy_out="$(node "$ROOT/scripts/privacy-banned-token-check.mjs" "$ROOT" 2>&1)"; then + if [[ -n "$privacy_out" ]]; then + printf '%s\n' "$privacy_out" >&2 + fi ok "privacy banned-token scan clean" else while IFS= read -r line; do diff --git a/scripts/lib/skill-lists.sh b/scripts/lib/skill-lists.sh index 0f6864a..f2dc79c 100644 --- a/scripts/lib/skill-lists.sh +++ b/scripts/lib/skill-lists.sh @@ -108,6 +108,7 @@ CRITICAL_SCRIPTS=( deep-audit-artifact-check.mjs plan-readiness-check.mjs performance-baseline.mjs + privacy-banned-token-check.mjs pr-preflight.mjs live-hook-noise-report.mjs session-audit.mjs @@ -153,6 +154,7 @@ INSTALL_SCRIPTS=( deep-audit-artifact-check.mjs plan-readiness-check.mjs performance-baseline.mjs + privacy-banned-token-check.mjs pr-preflight.mjs agent-task-packet-check.mjs guard-override-token.mjs diff --git a/scripts/privacy-banned-token-check.mjs b/scripts/privacy-banned-token-check.mjs new file mode 100644 index 0000000..454d5bd --- /dev/null +++ b/scripts/privacy-banned-token-check.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node +import { existsSync, readFileSync, statSync } from 'node:fs'; +import { extname, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { TextDecoder } from 'node:util'; + +const root = resolve(process.argv[2] || process.cwd()); +const manifestPath = resolve(root, 'rules-manifest.json'); + +function readUtf8(filePath) { + const bytes = readFileSync(filePath); + try { + return new TextDecoder('utf-8', { fatal: true }).decode(bytes); + } catch (error) { + console.error(`warning: encoding issue while reading ${filePath}: ${error.message}`); + return new TextDecoder('utf-8').decode(bytes); + } +} + +function git(args) { + return spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); +} + +function gitLines(args) { + const result = git(args); + if (result.status !== 0) { + throw new Error((result.stderr || result.stdout || `git ${args.join(' ')} failed`).trim()); + } + return result.stdout.split(/\r?\n/).filter(Boolean); +} + +function arrayValue(value) { + return Array.isArray(value) ? value : []; +} + +function localTokensFromJson(filePath, relPath) { + let parsed; + try { + parsed = JSON.parse(readUtf8(filePath)); + } catch (error) { + throw new Error(`malformed JSON in ${relPath}: ${error.message}`); + } + if (Array.isArray(parsed)) return parsed; + return arrayValue(parsed?.privacy?.bannedTokens || parsed?.bannedTokens); +} + +function localTokensFromText(filePath) { + return readUtf8(filePath) + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')); +} + +try { + const manifest = JSON.parse(readUtf8(manifestPath)); + const privacy = manifest.privacy || {}; + const localTokenFiles = new Set(arrayValue(privacy.localTokenFiles).map(String)); + const tokens = arrayValue(privacy.bannedTokens) + .map((token) => String(token).trim().toLowerCase()) + .filter(Boolean); + + for (const relPath of [...localTokenFiles].sort()) { + const ignored = git(['check-ignore', '--quiet', '--', relPath]); + if (ignored.status !== 0) { + throw new Error(`local privacy token file is not gitignored: ${relPath}`); + } + } + + for (const relPath of localTokenFiles) { + const localPath = resolve(root, relPath); + if (!existsSync(localPath)) continue; + const localTokens = extname(localPath) === '.json' + ? localTokensFromJson(localPath, relPath) + : localTokensFromText(localPath); + for (const token of localTokens) { + const normalized = String(token).trim().toLowerCase(); + if (normalized) tokens.push(normalized); + } + } + + const uniqueTokens = [...new Set(tokens)].sort(); + const violations = []; + for (const relPath of gitLines(['ls-files'])) { + if (relPath === 'rules-manifest.json' || localTokenFiles.has(relPath)) continue; + const filePath = resolve(root, relPath); + try { + if (statSync(filePath).size > 10 * 1024 * 1024) continue; + const text = readUtf8(filePath).toLowerCase(); + const foundCount = uniqueTokens.filter((token) => text.includes(token)).length; + if (foundCount) violations.push(`${relPath}: banned token match count=${foundCount}`); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + } + + if (violations.length) { + console.error(violations.join('\n')); + process.exit(1); + } +} catch (error) { + console.error(error.message); + process.exit(1); +} diff --git a/tests/test-workflow-tools.sh b/tests/test-workflow-tools.sh index 526171d..9407aa9 100755 --- a/tests/test-workflow-tools.sh +++ b/tests/test-workflow-tools.sh @@ -442,6 +442,7 @@ fi assert_command "pr preflight syntax" node --check "$ROOT/scripts/pr-preflight.mjs" assert_command "live hook noise report syntax" node --check "$ROOT/scripts/live-hook-noise-report.mjs" assert_command "session audit syntax" node --check "$ROOT/scripts/session-audit.mjs" +assert_command "privacy banned token check syntax" node --check "$ROOT/scripts/privacy-banned-token-check.mjs" assert_command "performance baseline syntax" node --check "$ROOT/scripts/performance-baseline.mjs" assert_command "disk cleanup manifest syntax" node --check "$ROOT/scripts/disk-cleanup-manifest.mjs" assert_command "pr preflight validates fixture" bash -c "printf '%s\n' '{\"branch\":\"feature\",\"dirty\":false,\"changedFiles\":[],\"blockers\":[],\"ghAvailable\":false}' | node \"\$0/scripts/pr-preflight.mjs\" validate --json >/dev/null" "$ROOT" @@ -543,6 +544,78 @@ if disk_denormalized_path="$(printf '%s\n' "$disk_manifest_denormalized_path" | else assert_contains "disk cleanup manifest rejects denormalized absolute paths" "$disk_denormalized_path" "path must be absolute" fi +privacy_repo="$TMPROOT/privacy-token-check-repo" +mkdir -p "$privacy_repo/.eternal" "$privacy_repo/docs" +git -C "$privacy_repo" init -q +git -C "$privacy_repo" config user.email test@example.com +git -C "$privacy_repo" config user.name Test +cat >"$privacy_repo/.gitignore" <<'EOF' +/.eternal/privacy-banned-tokens.local +/.eternal/privacy-banned-tokens-extra.json +/rules-manifest.local.json +EOF +cat >"$privacy_repo/rules-manifest.json" <<'JSON' +{ + "schemaVersion": 1, + "privacy": { + "bannedTokens": ["TRACKED_PRIVATE_TOKEN"], + "localTokenFiles": [ + ".eternal/privacy-banned-tokens.local", + ".eternal/privacy-banned-tokens-extra.json", + "rules-manifest.local.json" + ] + } +} +JSON +printf '%s\n' '# comment' 'LOCAL_LINE_TOKEN' >"$privacy_repo/.eternal/privacy-banned-tokens.local" +printf '%s\n' '{"bannedTokens":["LOCAL_TOP_TOKEN"]}' >"$privacy_repo/.eternal/privacy-banned-tokens-extra.json" +printf '%s\n' '{"privacy":{"bannedTokens":["LOCAL_NESTED_TOKEN"]}}' >"$privacy_repo/rules-manifest.local.json" +printf '%s\n' 'TRACKED_PRIVATE_TOKEN LOCAL_LINE_TOKEN LOCAL_TOP_TOKEN LOCAL_NESTED_TOKEN' >"$privacy_repo/docs/leak.md" +git -C "$privacy_repo" add .gitignore rules-manifest.json docs/leak.md +if privacy_violation_out="$(node "$ROOT/scripts/privacy-banned-token-check.mjs" "$privacy_repo" 2>&1)"; then + not_ok "privacy banned-token check reports tracked and local token matches" +else + assert_contains "privacy banned-token check reports tracked and local token matches" "$privacy_violation_out" "docs/leak.md: banned token match count=4" +fi + +privacy_ignore_repo="$TMPROOT/privacy-token-ignore-repo" +mkdir -p "$privacy_ignore_repo" +git -C "$privacy_ignore_repo" init -q +cat >"$privacy_ignore_repo/rules-manifest.json" <<'JSON' +{ + "schemaVersion": 1, + "privacy": { + "bannedTokens": [], + "localTokenFiles": ["not-ignored.local"] + } +} +JSON +git -C "$privacy_ignore_repo" add rules-manifest.json +if privacy_ignore_out="$(node "$ROOT/scripts/privacy-banned-token-check.mjs" "$privacy_ignore_repo" 2>&1)"; then + not_ok "privacy banned-token check requires local token files to be gitignored" +else + assert_contains "privacy banned-token check requires local token files to be gitignored" "$privacy_ignore_out" "local privacy token file is not gitignored" +fi + +privacy_encoding_repo="$TMPROOT/privacy-token-encoding-repo" +mkdir -p "$privacy_encoding_repo/docs" +git -C "$privacy_encoding_repo" init -q +cat >"$privacy_encoding_repo/rules-manifest.json" <<'JSON' +{ + "schemaVersion": 1, + "privacy": { + "bannedTokens": ["NO_MATCH_TOKEN"], + "localTokenFiles": [] + } +} +JSON +printf '\xffnot a token\n' >"$privacy_encoding_repo/docs/binary.txt" +git -C "$privacy_encoding_repo" add rules-manifest.json docs/binary.txt +if privacy_encoding_out="$(node "$ROOT/scripts/privacy-banned-token-check.mjs" "$privacy_encoding_repo" 2>&1)"; then + assert_contains "privacy banned-token check warns on encoding issues" "$privacy_encoding_out" "warning: encoding issue" +else + not_ok "privacy banned-token check warns on encoding issues" +fi assert_command "deep-stack artifact library syntax" node --check "$ROOT/scripts/lib/deep-stack-artifacts.mjs" assert_command "deep-audit artifact check syntax" node --check "$ROOT/scripts/deep-audit-artifact-check.mjs" assert_command "deep-audit category registry syntax" node --check "$ROOT/scripts/lib/deep-audit-categories.mjs" @@ -1858,15 +1931,14 @@ if [[ -f "$INIT_SCRIPT" ]]; then mkdir -p "$tcg_target" bash "$INIT_SCRIPT" --profile eternal-saas-tcg "$tcg_target" >/dev/null 2>&1 || true assert_file "init tcg profile installs tcg contract module" "$tcg_target/.claude/rules/eternal-saas/project/tcg-contract.md" - # simulate manifest bump by touching source (sleep ensures different mtime second) + # simulate manifest bump with an explicit future mtime; restore the source after the check. orpc_source="$ROOT/rules/eternal-saas/project/orpc.md" orpc_mtime_marker="$TMPROOT/orpc-source-mtime" if [[ -f "$orpc_source" ]]; then touch "$orpc_mtime_marker" touch -r "$orpc_source" "$orpc_mtime_marker" trap 'touch -r "$orpc_mtime_marker" "$orpc_source" 2>/dev/null || true; cc_test_cleanup' EXIT - sleep 1 - touch "$orpc_source" + touch -t 203001010000 "$orpc_source" check_out="$(bash "$INIT_SCRIPT" --check --profile eternal-saas "$real_target" 2>&1)" || true assert_contains "init --check reports stale after manifest bump" "$check_out" "stale" touch -r "$orpc_mtime_marker" "$orpc_source" From 34e46451e9b5b2f8c264a1fc9aa112ff5db1fab2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 20:02:36 -0700 Subject: [PATCH 18/24] address final coderabbit review notes --- docs/skills.md | 2 +- scripts/bootstrap-tools.sh | 16 +++++++-- scripts/doctor.sh | 4 +-- scripts/init-project-rules.sh | 12 ++++++- scripts/tool-stack-check.mjs | 14 ++++++-- .../brooks-audit/architecture-guide.md | 2 +- .../bundled/eternal-best-practices/SKILL.md | 2 +- tests/test-workflow-tools.sh | 35 +++++++++++++++++++ 8 files changed, 76 insertions(+), 11 deletions(-) diff --git a/docs/skills.md b/docs/skills.md index 08029e8..66ca9dd 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -147,7 +147,7 @@ Eternal Stack installs two cooperating layers: Bundled skills are vendored under `skills/bundled//` in this repository. `scripts/install.sh` copies each tree to `~/.claude/skills/` and `~/.codex/skills/`. Maintainers refresh vendored copies from canonical host trees with `scripts/vendor-bundled-skills.sh`. -The `eternal-saas-tcg` rule profile adds `rules/eternal-saas/project/tcg-contract.md` for TCG repositories. That rule routes TCG-domain prompts into the same bundled policy and domain-skill enumeration as `eternal-best-practices` and the `domain-*` skills. +The `eternal-saas-tcg` rule profile adds `rules/eternal-saas/project/tcg-contract.md` for TCG repositories. That rule routes TCG-domain prompts into the same bundled policy and domain-skill enumeration as `eternal-best-practices`, `domain-cli`, `domain-cloud-native`, `domain-embedded`, `domain-fintech`, `domain-iot`, `domain-ml`, and `domain-web`. When the same guidance exists under `skills/etrnl-*/references/`, prefer the repo module first; load the bundled skill when the task needs the full surface or hooks require it by name. diff --git a/scripts/bootstrap-tools.sh b/scripts/bootstrap-tools.sh index ab4ae83..8f60999 100755 --- a/scripts/bootstrap-tools.sh +++ b/scripts/bootstrap-tools.sh @@ -14,12 +14,24 @@ SKIP_HINDSIGHT=0 DRY_RUN=0 # Admin-tool npm specs: set ETRNL_*_NPM_SPEC only from trusted administrator-controlled input. # Unsanitized values enable arbitrary command execution through shell interpolation. -CODEGRAPH_NPM_SPEC="${ETRNL_CODEGRAPH_NPM_SPEC:-@colbymchenry/codegraph@1.0.1}" -BEADS_NPM_SPEC="${ETRNL_BEADS_NPM_SPEC:-@beads/bd@1.0.5}" +CODEGRAPH_NPM_SPEC="${ETRNL_CODEGRAPH_NPM_SPEC-@colbymchenry/codegraph@1.0.1}" +BEADS_NPM_SPEC="${ETRNL_BEADS_NPM_SPEC-@beads/bd@1.0.5}" CONFIRM_SKIPPED=64 PROFILE="${ETRNL_STACK_PROFILE:-core}" HINDSIGHT_MODE="${ETRNL_HINDSIGHT_MODE:-local-daemon}" +validate_npm_spec() { + local name="$1" + local value="$2" + if [[ ! "$value" =~ ^(@[A-Za-z0-9._-]+/[A-Za-z0-9._-]+|[A-Za-z0-9._-]+)(@[A-Za-z0-9._~+-]+)?$ ]]; then + printf 'bootstrap error: unsafe %s npm spec: %s\n' "$name" "${value:-}" >&2 + exit 2 + fi +} + +validate_npm_spec ETRNL_CODEGRAPH_NPM_SPEC "$CODEGRAPH_NPM_SPEC" +validate_npm_spec ETRNL_BEADS_NPM_SPEC "$BEADS_NPM_SPEC" + usage() { cat <<'EOF' Usage: bootstrap-tools.sh install|check|project [options] diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 9b96de0..ad8ddee 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -837,9 +837,7 @@ if git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1 && [[ -f "$ROO ok "credential pattern scan clean" fi if [[ -f "$ROOT/rules-manifest.json" ]]; then - if ! command -v python3 >/dev/null 2>&1; then - fail "python3 unavailable; privacy banned-token scan cannot run" - elif privacy_out="$(node "$ROOT/scripts/privacy-banned-token-check.mjs" "$ROOT" 2>&1)"; then + if privacy_out="$(node "$ROOT/scripts/privacy-banned-token-check.mjs" "$ROOT" 2>&1)"; then if [[ -n "$privacy_out" ]]; then printf '%s\n' "$privacy_out" >&2 fi diff --git a/scripts/init-project-rules.sh b/scripts/init-project-rules.sh index 9dfa985..d061470 100755 --- a/scripts/init-project-rules.sh +++ b/scripts/init-project-rules.sh @@ -56,6 +56,16 @@ file_sha256() { fi } +iso_to_epoch() { + python3 -c " +from datetime import datetime +import sys +value = sys.argv[1] +value = value[:-1] + '+00:00' if value.endswith('Z') else value +print(int(datetime.fromisoformat(value).timestamp())) +" "$1" 2>/dev/null || echo "0" +} + receipt_checksum_for() { local sums="$1" local rel="$2" @@ -198,7 +208,7 @@ if [[ "$CHECK_MODE" -eq 1 ]]; then fi if [[ -n "$install_ts" ]]; then src_mtime="$(python3 -c "import os, sys; print(int(os.path.getmtime(sys.argv[1])))" "$src" 2>/dev/null || echo "0")" - install_epoch="$(python3 -c "from datetime import datetime; import sys; value = sys.argv[1]; value = value[:-1] + '+00:00' if value.endswith('Z') else value; print(int(datetime.fromisoformat(value).timestamp()))" "$install_ts" 2>/dev/null || echo "0")" + install_epoch="$(iso_to_epoch "$install_ts")" if [[ "$src_mtime" -gt "$install_epoch" ]]; then echo "stale: $rel" any_stale=1 diff --git a/scripts/tool-stack-check.mjs b/scripts/tool-stack-check.mjs index e18cd85..c15bdd2 100755 --- a/scripts/tool-stack-check.mjs +++ b/scripts/tool-stack-check.mjs @@ -24,9 +24,19 @@ const explainMode = hasFlag("--explain"); const force = hasFlag("--force"); const projectPath = valueAfter("--project", valueAfter("--cwd", "")); const hindsightStrictChecks = /^(1|true|yes)$/i.test(process.env.HINDSIGHT_STRICT_CHECKS || ""); +const npmSpecPattern = /^(@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+|[A-Za-z0-9._-]+)(@[A-Za-z0-9._~+-]+)?$/; +function npmSpecFromEnv(name, fallback) { + if (!Object.prototype.hasOwnProperty.call(process.env, name)) return fallback; + const value = process.env[name] || ""; + if (!npmSpecPattern.test(value)) { + console.error(`unsafe ${name} npm spec: ${value || ""}`); + process.exit(2); + } + return value; +} const toolSpecs = { - codegraph: process.env.ETRNL_CODEGRAPH_NPM_SPEC || "@colbymchenry/codegraph@1.0.1", - beads: process.env.ETRNL_BEADS_NPM_SPEC || "@beads/bd@1.0.5", + codegraph: npmSpecFromEnv("ETRNL_CODEGRAPH_NPM_SPEC", "@colbymchenry/codegraph@1.0.1"), + beads: npmSpecFromEnv("ETRNL_BEADS_NPM_SPEC", "@beads/bd@1.0.5"), }; function usage() { diff --git a/skills/bundled/brooks-audit/architecture-guide.md b/skills/bundled/brooks-audit/architecture-guide.md index 10cc1fc..826a5ef 100644 --- a/skills/bundled/brooks-audit/architecture-guide.md +++ b/skills/bundled/brooks-audit/architecture-guide.md @@ -103,7 +103,7 @@ Rules: 3. **Edges** - Solid arrows (`-->`) point FROM the depending module TO the dependency; use dotted arrows with label (`-.->|circular|`) for circular dependencies. If no circular dependencies exist, use only solid arrows 4. **Node limit** - Keep the graph to ~50 nodes maximum; collapse low-risk leaf modules into their parent if needed 5. **Fan-out** - For any node with fan-out > 5, use a descriptive label: `HighFanOutModule["ModuleName (fan-out: 7)"]` -6. **Colors** - Apply `classDef` colors AFTER completing Steps 2-4: `critical` (red `#ff6b6b`) for nodes with Critical findings, `warning` (yellow `#ffd43b`) for Warning findings, `clean` (green `#51cf66`) for nodes with no findings or only Rules. If no findings at all, classify all nodes as `clean` +6. **Colors** - Apply `classDef` colors AFTER completing Steps 2-4: `critical` (red `#ff6b6b`) for nodes with Critical findings, `warning` (yellow `#ffd43b`) for Warning findings, `clean` (green `#51cf66`) for nodes with no findings. If no findings at all, classify all nodes as `clean` 7. **Direction** - Default to `graph TD` (top-down); use `graph LR` only if the architecture is clearly a left-to-right pipeline ### Step 2: Scan for Dependency Disorder diff --git a/skills/bundled/eternal-best-practices/SKILL.md b/skills/bundled/eternal-best-practices/SKILL.md index b7abd5c..dafb87f 100644 --- a/skills/bundled/eternal-best-practices/SKILL.md +++ b/skills/bundled/eternal-best-practices/SKILL.md @@ -1583,7 +1583,7 @@ export const campaignsRouter = router({ ```typescript // packages/queue/src/index.ts (BullMQ setup) import { Queue } from 'bullmq'; -import { redis } from '@example-suite/db'; +import { redis } from '@example-suite/db/client'; export const emailQueue = new Queue('email', { connection: redis }); export const notaFiscalQueue = new Queue('nota-fiscal', { connection: redis }); diff --git a/tests/test-workflow-tools.sh b/tests/test-workflow-tools.sh index 9407aa9..6386613 100755 --- a/tests/test-workflow-tools.sh +++ b/tests/test-workflow-tools.sh @@ -851,6 +851,24 @@ assert_json_expr "tool stack checker detects codegraph update" "$tool_stack_json assert_json_expr "tool stack checker uses pinned global npm specs" "$tool_stack_json" '(.tools.codegraph.updateCommand | contains("@colbymchenry/codegraph@1.0.1")) and (.tools.beads.updateCommand | contains("@beads/bd@1.0.5"))' assert_json_expr "tool stack checker keeps beads current" "$tool_stack_json" '.tools.beads.installed == true and .tools.beads.currentVersion == "1.0.5" and .tools.beads.updateAvailable == false' assert_json_expr "tool stack checker reports Hindsight plugin posture" "$tool_stack_json" '.tools.hindsight.pluginEnabled == true and .tools.hindsight.pluginInstalled == true and .tools.hindsight.ok == true and .tools.hindsight.mode == "local-daemon"' +unsafe_npm_specs=('@scope/pkg;rm' '@scope/pkg|cat' '@scope/pkg&cat' "\$(touch hacked)" "\`touch hacked\`" '') +tool_stack_unsafe_error="" +for env_name in ETRNL_CODEGRAPH_NPM_SPEC ETRNL_BEADS_NPM_SPEC; do + for unsafe_spec in "${unsafe_npm_specs[@]}"; do + if unsafe_out="$(env "$env_name=$unsafe_spec" PATH="$tool_stack_bin:/usr/bin:/bin" "$node_bin" "$ROOT/scripts/tool-stack-check.mjs" --json --force 2>&1)"; then + tool_stack_unsafe_error="$env_name accepted ${unsafe_spec:-}" + break 2 + elif [[ "$unsafe_out" != *"unsafe $env_name npm spec"* ]]; then + tool_stack_unsafe_error="$env_name did not report unsafe spec for ${unsafe_spec:-}: $unsafe_out" + break 2 + fi + done +done +if [[ -z "$tool_stack_unsafe_error" ]]; then + ok "tool stack checker rejects unsafe npm specs" +else + not_ok "tool stack checker rejects unsafe npm specs: $tool_stack_unsafe_error" +fi mkdir -p "$TMPROOT/tool-stack-home/plugins/cache/hindsight/hindsight-memory/0.7.1/hooks" printf '{}\n' >"$TMPROOT/tool-stack-home/plugins/cache/hindsight/hindsight-memory/0.7.1/hooks/hooks.json" hindsight_cache_json="$(PATH="/usr/bin:/bin" CLAUDE_HOME="$TMPROOT/tool-stack-home" HINDSIGHT_HOME="$TMPROOT/tool-stack-hindsight" ETRNL_TOOL_STACK_STATE="$TMPROOT/tool-stack-cache-state.json" "$node_bin" "$ROOT/scripts/tool-stack-check.mjs" --json --force)" @@ -1011,6 +1029,23 @@ dirty_skipped_json="$(CLAUDE_HOME="$dirty_auto_home" CODEX_HOME="$TMPROOT/dirty- assert_json_expr "update-check skips auto-update on dirty source checkout" "$dirty_skipped_json" '.autoUpdate | startswith("ETRNL_AUTO_UPDATE_SKIPPED")' assert_no_file "update-check dirty skip does not run updater" "$dirty_auto_home/auto-update-dirty-ran" assert_command "bootstrap tools shell syntax" bash -n "$ROOT/scripts/bootstrap-tools.sh" +bootstrap_unsafe_error="" +for env_name in ETRNL_CODEGRAPH_NPM_SPEC ETRNL_BEADS_NPM_SPEC; do + for unsafe_spec in "${unsafe_npm_specs[@]}"; do + if bootstrap_out="$(env "$env_name=$unsafe_spec" bash "$ROOT/scripts/bootstrap-tools.sh" install --profile full --dry-run 2>&1)"; then + bootstrap_unsafe_error="$env_name accepted ${unsafe_spec:-}" + break 2 + elif [[ "$bootstrap_out" != *"unsafe $env_name npm spec"* ]]; then + bootstrap_unsafe_error="$env_name did not report unsafe spec for ${unsafe_spec:-}: $bootstrap_out" + break 2 + fi + done +done +if [[ -z "$bootstrap_unsafe_error" ]]; then + ok "bootstrap tools rejects unsafe npm specs" +else + not_ok "bootstrap tools rejects unsafe npm specs: $bootstrap_unsafe_error" +fi merge_target="$TMPROOT/settings-target.json" merge_template="$TMPROOT/settings-template.json" printf '%s\n' "{\"hooks\":{\"SessionStart\":[{\"hooks\":[{\"type\":\"command\",\"command\":\"bash ~/.claude/hooks/cc-sessionstart-restore.sh\",\"timeout\":5}]},{\"hooks\":[{\"type\":\"command\",\"command\":\"bash $HOME/.claude/hooks/cc-sessionstart-restore.sh\",\"timeout\":7}]}],\"Stop\":[{\"hooks\":[{\"type\":\"command\",\"command\":\"bash $HOME/.claude/hooks/cc-stop-verifier.sh\",\"timeout\":5}]},{\"hooks\":[{\"type\":\"command\",\"command\":\"bash ~/.claude/hooks/cc-stop-verifier.sh\",\"timeout\":10}]},{\"hooks\":[{\"type\":\"command\",\"command\":\"bash /tmp$HOME/.claude/hooks/not-real.sh\",\"timeout\":1}]}]}}" >"$merge_target" From 331657460cafb311e7ce36b4971af252053e7f30 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 20:14:17 -0700 Subject: [PATCH 19/24] resolve coderabbit changelog follow-ups --- CHANGELOG.md | 5 ++++- docs/skills.md | 2 +- scripts/doctor.sh | 2 +- scripts/tool-stack-check.mjs | 6 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ed4c76..696922d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this ### Added - `.github/workflows/health.yml` — CI health workflow for rule export sync, hook tests, workflow tests, install/rollback tests, and doctor. +- `scripts/doctor.sh` — health checks now cover ShellCheck, rule module export drift, privacy scan enforcement, and pending-release changelog validation. - `rules/eternal-saas/project/tcg-contract.md` — scoped TCG/card-domain contract rule module and generated Cursor export. ### Changed @@ -33,6 +34,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this - `scripts/init-project-rules.sh` — installs generated Cursor `.mdc` modules alongside Claude rules, validates Cursor exports, and tracks Cursor checksums in the install receipt. - `rules/eternal-saas/*` — rule host metadata now reflects Claude and Cursor support without claiming unsupported Codex nested context output. - Email triage runtime references now use the `etrnl-email` command pattern across guards, canaries, slash commands, and fixtures instead of legacy `vivaz-email` naming. +- `hooks/cc-sessionstart-restore.sh`, `scripts/lib/etrnl-state-core.mjs`, and `scripts/workflow-health.mjs` — track session reset boundaries so `/new` and `/clear` isolate stale compact handoff state. +- `scripts/bootstrap-tools.sh` and `scripts/tool-stack-check.mjs` — support validated `ETRNL_CODEGRAPH_NPM_SPEC` and `ETRNL_BEADS_NPM_SPEC` overrides for pinned global tool installs. - Bundled skill namespaces now align around `@example-suite`, `money-vo-discipline`, and `orpc-patterns` naming across policy skills, routing lists, and vendored bundles. - `skills/bundled/stripe-best-practices` — hardens Stripe guidance from advisory wording to explicit policy gates for API versions, payment-surface selection, test/migration expectations, and Connect settlement/dispute behavior. @@ -44,7 +47,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this ### Security -- `rules-manifest.json` and `scripts/doctor.sh` — remove tracked private project literals from the privacy gate and support gitignored local banned-token overlays with redacted diagnostics. +- `rules-manifest.json`, `scripts/privacy-banned-token-check.mjs`, and `scripts/doctor.sh` — remove tracked private project literals from the privacy gate and support standalone or doctor-integrated banned-token scans with gitignored local overlays and redacted diagnostics. ## v0.5.1 diff --git a/docs/skills.md b/docs/skills.md index 66ca9dd..d2d6f0d 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -147,7 +147,7 @@ Eternal Stack installs two cooperating layers: Bundled skills are vendored under `skills/bundled//` in this repository. `scripts/install.sh` copies each tree to `~/.claude/skills/` and `~/.codex/skills/`. Maintainers refresh vendored copies from canonical host trees with `scripts/vendor-bundled-skills.sh`. -The `eternal-saas-tcg` rule profile adds `rules/eternal-saas/project/tcg-contract.md` for TCG repositories. That rule routes TCG-domain prompts into the same bundled policy and domain-skill enumeration as `eternal-best-practices`, `domain-cli`, `domain-cloud-native`, `domain-embedded`, `domain-fintech`, `domain-iot`, `domain-ml`, and `domain-web`. +The `eternal-saas-tcg` rules profile is not a bundled skill. It adds `rules/eternal-saas/project/tcg-contract.md` for TCG repositories, and that rule routes TCG-domain prompts into the same bundled policy and domain-skill enumeration as `eternal-best-practices`, `domain-cli`, `domain-cloud-native`, `domain-embedded`, `domain-fintech`, `domain-iot`, `domain-ml`, and `domain-web`. When the same guidance exists under `skills/etrnl-*/references/`, prefer the repo module first; load the bundled skill when the task needs the full surface or hooks require it by name. diff --git a/scripts/doctor.sh b/scripts/doctor.sh index ad8ddee..52dde03 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -42,7 +42,7 @@ DOCTOR_HEAVY_STARTED=0 # shellcheck source=scripts/lib/skill-lists.sh source "$ROOT/scripts/lib/skill-lists.sh" -trap 'rm -rf -- "$DOCTOR_RESULT_DIR"' EXIT +trap '[[ -n "${DOCTOR_RESULT_DIR:-}" ]] && rm -rf -- "$DOCTOR_RESULT_DIR"' EXIT ok() { printf 'ok: %s\n' "$*"; } fail() { printf 'fail: %s\n' "$*" >&2; STATUS=1; } diff --git a/scripts/tool-stack-check.mjs b/scripts/tool-stack-check.mjs index c15bdd2..875f02a 100755 --- a/scripts/tool-stack-check.mjs +++ b/scripts/tool-stack-check.mjs @@ -435,9 +435,9 @@ function failedProjectStatus(resolved, message) { } const tools = [ - // Admin-tool install commands interpolate trusted user environment specs. - // Set ETRNL_*_NPM_SPEC only from trusted administrator-controlled input; - // shell metacharacters in those values would execute locally by design. + // Admin-tool install commands interpolate environment specs after regex + // validation blocks shell metacharacters; trusted administrator-controlled + // input remains an additional defense in depth. { id: "codegraph", command: "codegraph", From 2947e750dc5235df6c15d7236ed2567a08392efe Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 20:28:57 -0700 Subject: [PATCH 20/24] address coderabbit documentation and sync notes --- rules/etrnl/domains.md | 2 +- scripts/sync-rule-exports.mjs | 5 +++-- skills/bundled/brooks-audit/architecture-guide.md | 2 +- skills/bundled/orpc-patterns/references/getting-started.md | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/rules/etrnl/domains.md b/rules/etrnl/domains.md index e0e90a4..ecd25ea 100644 --- a/rules/etrnl/domains.md +++ b/rules/etrnl/domains.md @@ -2,7 +2,7 @@ - Invoke `eternal-best-practices` before editing domain-sensitive surfaces. - Domain-sensitive surfaces include auth, tenancy, billing, payments, money values, i18n, Prisma/schema, permissions, and soft deletes. -- Use bundled stack skills: `eternal-best-practices`, `code-simplifier`, `finding-duplicate-functions`, and Brooks modules via `/etrnl-code-review-excellence`. +- Use bundled stack skills: `eternal-best-practices`, `code-simplifier`, `finding-duplicate-functions`, and Brooks modules via `etrnl-code-review-excellence/references/`. - For domain-sensitive work, use `domain-cli`, `domain-cloud-native`, `domain-embedded`, `domain-fintech`, `domain-iot`, `domain-ml`, and `domain-web`. - For auth, tenancy, money, Prisma, i18n, and payments work, use `better-auth`, `tenant-isolation-patterns`, `money-vo-discipline`, `prisma-expert`, `i18n-localization`, `stripe-best-practices`, and `abacatepay-integration`. - If a bundled skill is missing from the host, state that explicitly and run the closest manual review pass. diff --git a/scripts/sync-rule-exports.mjs b/scripts/sync-rule-exports.mjs index b1fabdb..659c578 100755 --- a/scripts/sync-rule-exports.mjs +++ b/scripts/sync-rule-exports.mjs @@ -9,7 +9,7 @@ */ import { createHash } from 'node:crypto'; -import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'node:fs'; +import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, lstatSync } from 'node:fs'; import { resolve, dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -192,10 +192,11 @@ function walkMd(dir) { const full = join(dir, entry); let stat; try { - stat = statSync(full); + stat = lstatSync(full); } catch (error) { throw new Error(`cannot stat rule path ${full}: ${error.message}`); } + if (stat.isSymbolicLink()) continue; if (stat.isDirectory()) files.push(...walkMd(full)); else if (entry.endsWith('.md')) files.push(full); } diff --git a/skills/bundled/brooks-audit/architecture-guide.md b/skills/bundled/brooks-audit/architecture-guide.md index 826a5ef..c137d8f 100644 --- a/skills/bundled/brooks-audit/architecture-guide.md +++ b/skills/bundled/brooks-audit/architecture-guide.md @@ -103,7 +103,7 @@ Rules: 3. **Edges** - Solid arrows (`-->`) point FROM the depending module TO the dependency; use dotted arrows with label (`-.->|circular|`) for circular dependencies. If no circular dependencies exist, use only solid arrows 4. **Node limit** - Keep the graph to ~50 nodes maximum; collapse low-risk leaf modules into their parent if needed 5. **Fan-out** - For any node with fan-out > 5, use a descriptive label: `HighFanOutModule["ModuleName (fan-out: 7)"]` -6. **Colors** - Apply `classDef` colors AFTER completing Steps 2-4: `critical` (red `#ff6b6b`) for nodes with Critical findings, `warning` (yellow `#ffd43b`) for Warning findings, `clean` (green `#51cf66`) for nodes with no findings. If no findings at all, classify all nodes as `clean` +6. **Colors** - Apply `classDef` colors AFTER completing Steps 2-4: `critical` (red `#ff6b6b`) for nodes with Critical findings, `warning` (yellow `#ffd43b`) for Warning or Rules findings, `clean` (green `#51cf66`) only for nodes with no findings. If no findings at all, classify all nodes as `clean` 7. **Direction** - Default to `graph TD` (top-down); use `graph LR` only if the architecture is clearly a left-to-right pipeline ### Step 2: Scan for Dependency Disorder diff --git a/skills/bundled/orpc-patterns/references/getting-started.md b/skills/bundled/orpc-patterns/references/getting-started.md index 2b6ff9b..74ff67a 100644 --- a/skills/bundled/orpc-patterns/references/getting-started.md +++ b/skills/bundled/orpc-patterns/references/getting-started.md @@ -2,7 +2,7 @@ ## Prerequisites -- Node.js 18+ works; Node.js 24+ is default for new projects. +- Node.js 18+ works; bundled CI templates default to Node.js 20 unless a project overrides them. - Bun, Deno, and Cloudflare Workers are supported through adapters. - TypeScript strict mode is documented. - oRPC supports Zod, Valibot, Arktype, and any Standard Schema implementation. From e29dd3ae7e721c32e9e3a8a64ed886a7004d9534 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 20:46:44 -0700 Subject: [PATCH 21/24] harden privacy scanner review notes --- scripts/privacy-banned-token-check.mjs | 11 ++++++++++- scripts/sync-rule-exports.mjs | 3 ++- templates/AGENTS.override.codex.md | 2 +- tests/test-workflow-tools.sh | 19 +++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/scripts/privacy-banned-token-check.mjs b/scripts/privacy-banned-token-check.mjs index 454d5bd..d858b22 100644 --- a/scripts/privacy-banned-token-check.mjs +++ b/scripts/privacy-banned-token-check.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node import { existsSync, readFileSync, statSync } from 'node:fs'; -import { extname, resolve } from 'node:path'; +import { extname, isAbsolute, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; import { TextDecoder } from 'node:util'; @@ -33,6 +33,10 @@ function arrayValue(value) { return Array.isArray(value) ? value : []; } +function isSafeRelativePath(relPath) { + return !isAbsolute(relPath) && !relPath.split(/[\\/]+/).includes('..'); +} + function localTokensFromJson(filePath, relPath) { let parsed; try { @@ -60,6 +64,9 @@ try { .filter(Boolean); for (const relPath of [...localTokenFiles].sort()) { + if (!isSafeRelativePath(relPath)) { + throw new Error(`local privacy token file must be a safe relative path: ${relPath}`); + } const ignored = git(['check-ignore', '--quiet', '--', relPath]); if (ignored.status !== 0) { throw new Error(`local privacy token file is not gitignored: ${relPath}`); @@ -84,6 +91,8 @@ try { if (relPath === 'rules-manifest.json' || localTokenFiles.has(relPath)) continue; const filePath = resolve(root, relPath); try { + // Performance guard: very large tracked files are skipped rather than fully decoded, + // so banned-token coverage intentionally applies to files at or below 10 MiB. if (statSync(filePath).size > 10 * 1024 * 1024) continue; const text = readUtf8(filePath).toLowerCase(); const foundCount = uniqueTokens.filter((token) => text.includes(token)).length; diff --git a/scripts/sync-rule-exports.mjs b/scripts/sync-rule-exports.mjs index 659c578..4b6ea1e 100755 --- a/scripts/sync-rule-exports.mjs +++ b/scripts/sync-rule-exports.mjs @@ -324,6 +324,7 @@ if (singleSource) { const rulesRoot = join(ROOT, 'rules', 'eternal-saas'); const cursorOutputRoot = join(ROOT, 'templates', 'cursor', 'rules', 'eternal-saas'); const expectedKeys = expectedModuleKeys(manifest); + const expectedKeySet = new Set(expectedKeys); let moduleFiles = []; try { moduleFiles = walkMd(rulesRoot); @@ -337,7 +338,7 @@ if (singleSource) { if (!fileKeys.has(key)) errors.push(`${key}: manifest profile references missing module source`); } for (const key of fileKeys) { - if (!expectedKeys.includes(key)) errors.push(`${key}: module source is not referenced by any manifest profile`); + if (!expectedKeySet.has(key)) errors.push(`${key}: module source is not referenced by any manifest profile`); } const moduleResults = []; diff --git a/templates/AGENTS.override.codex.md b/templates/AGENTS.override.codex.md index 954fa51..c3aa78b 100644 --- a/templates/AGENTS.override.codex.md +++ b/templates/AGENTS.override.codex.md @@ -15,4 +15,4 @@ Eternal Stack skills are available in `~/.codex/skills/etrnl-*`. Reference them ## Startup files -When installed: `~/.codex/AGENTS.md` provides the global baseline, `~/.codex/AGENTS.override.md` applies these Codex-specific overrides, and any project-level `AGENTS.md` in the repo root adds project context and overrides both. +When installed: `~/.codex/AGENTS.md` provides the global baseline and `~/.codex/AGENTS.override.md` applies these Codex-specific overrides. Eternal Stack does not generate project-level Codex `AGENTS.md` files from the rules manifest. diff --git a/tests/test-workflow-tools.sh b/tests/test-workflow-tools.sh index 6386613..112e7fd 100755 --- a/tests/test-workflow-tools.sh +++ b/tests/test-workflow-tools.sh @@ -597,6 +597,25 @@ else assert_contains "privacy banned-token check requires local token files to be gitignored" "$privacy_ignore_out" "local privacy token file is not gitignored" fi +privacy_unsafe_repo="$TMPROOT/privacy-token-unsafe-repo" +mkdir -p "$privacy_unsafe_repo" +git -C "$privacy_unsafe_repo" init -q +cat >"$privacy_unsafe_repo/rules-manifest.json" <<'JSON' +{ + "schemaVersion": 1, + "privacy": { + "bannedTokens": [], + "localTokenFiles": ["../outside.local"] + } +} +JSON +git -C "$privacy_unsafe_repo" add rules-manifest.json +if privacy_unsafe_out="$(node "$ROOT/scripts/privacy-banned-token-check.mjs" "$privacy_unsafe_repo" 2>&1)"; then + not_ok "privacy banned-token check rejects unsafe local token paths" +else + assert_contains "privacy banned-token check rejects unsafe local token paths" "$privacy_unsafe_out" "safe relative path" +fi + privacy_encoding_repo="$TMPROOT/privacy-token-encoding-repo" mkdir -p "$privacy_encoding_repo/docs" git -C "$privacy_encoding_repo" init -q From dff9c765c93fb42c7928e346aea06a7e321709f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 21:10:29 -0700 Subject: [PATCH 22/24] clarify ci cd enforcement coverage --- skills/bundled/ci-cd/SKILL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skills/bundled/ci-cd/SKILL.md b/skills/bundled/ci-cd/SKILL.md index 8830865..65afac6 100644 --- a/skills/bundled/ci-cd/SKILL.md +++ b/skills/bundled/ci-cd/SKILL.md @@ -32,10 +32,10 @@ For broad CI/CD upgrades, use this sequence: - Produce the same artifact for the same commit. Avoid mutable deploy inputs unless they are resolved to an immutable digest before rollout. - Never print, commit, or document secret values. Store secrets in GitHub/GitLab/Jenkins secret stores, cloud secret managers, Vault, or sealed secrets. -- Keep CI test secrets separate from staging/production secrets. CI must not need production credentials. -- Pin external actions and base images deliberately. Defaults to commit SHAs for GitHub Actions in high-trust repos and immutable image digests for production bases. -- Avoid direct `${{ github.event.* }}` interpolation inside shell `run:` blocks. Pass event data through env vars and quote carefully. -- Do not skip failing gates to go green. Fix root causes, move unsuitable checks to the right lane, or make them reference with an explicit plan to harden later. +- Keep CI test secrets separate from staging/production secrets. CI must not need production credentials. Manual review: validate the workflow architecture and secret boundaries. +- Pin external actions and base images deliberately. Defaults to commit SHAs for GitHub Actions in high-trust repos and immutable image digests for production bases. Automated support: `audit_github_actions.py`. +- Avoid direct `${{ github.event.* }}` interpolation inside shell `run:` blocks. Pass event data through env vars and quote carefully. Automated support: `audit_github_actions.py`. +- Do not skip failing gates to go green. Fix root causes, move unsuitable checks to the right lane, or make them reference with an explicit plan to harden later. Manual review: confirm gate ownership and remediation plans. - Treat flaky tests as production risks. Quarantine only with owner, ticket, expiry, and replacement coverage. ## Quality Gates From f274cfafacd13ed1181f59251b2d4c0d41aff749 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 21:24:11 -0700 Subject: [PATCH 23/24] normalize tcg contract coverage path --- docs/eternal-stack-coverage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/eternal-stack-coverage.md b/docs/eternal-stack-coverage.md index cc198ed..5a398e6 100644 --- a/docs/eternal-stack-coverage.md +++ b/docs/eternal-stack-coverage.md @@ -15,7 +15,7 @@ Status key: `done` means implemented in this repo; `live-gated` means intentiona | Durable artifacts | done: deep-stack bundle, sanitized source manifest, skill matrix, review phase records, TDD evidence, reuse inventory and reuse bindings, findings ledger, completion audit and completion reconciliation, TypeScript trigger evidence, install proof, Hybrid risk tier, review log, browser QA v1/v2 reports with mandatory console/network summaries, route/viewport matrix counts, screenshot hashes, capture freshness, and provenance for completed v2 runs, context save/restore, artifact-required ledger checks, redacted cross-session project buglog hints, workflow-health summaries/status JSON, and local tool-effectiveness verdict summaries. | | Agent templates | done: default-installed executor/reviewer/investigator/scout/adversary/design/DX/browser QA `etrnl-*` agents. | | Shared startup guidance | done: public `AGENTS.md` template plus tiny Claude wrapper importing it. | -| Rules | done: namespaced `rules/etrnl/*.md` to avoid clobbering existing user rules; cross-host eternal-saas pack (`rules/eternal-saas/global/` + `rules/eternal-saas/project/`, including `eternal-saas-tcg` and `project/tcg-contract`) with `rules-manifest.json` (schema v1, checksums, privacy gates); `scripts/init-project-rules.sh` deploys pack to any target repo with Claude+Cursor outputs; `scripts/sync-rule-exports.mjs` keeps `.mdc` Cursor twins in sync; doctor byte gate prevents startup guidance overflow. Reference: `docs/rules.md`. | +| Rules | done: namespaced `rules/etrnl/*.md` to avoid clobbering existing user rules; cross-host eternal-saas pack (`rules/eternal-saas/global/` + `rules/eternal-saas/project/`, including `eternal-saas-tcg` and `rules/eternal-saas/project/tcg-contract.md`) with `rules-manifest.json` (schema v1, checksums, privacy gates); `scripts/init-project-rules.sh` deploys pack to any target repo with Claude+Cursor outputs; `scripts/sync-rule-exports.mjs` keeps `.mdc` Cursor twins in sync; doctor byte gate prevents startup guidance overflow. Reference: `docs/rules.md`. | | Install/update/rollback/doctor | done: scripts, tests, rollback (including `rules/eternal-saas` plus startup file restore), canaries, rules, docs, templates, agents, settings audit/repair, installed update metadata, drift explain output, artifact helpers, post-upgrade browser-QA canary, installed-home doctor, and workflow-health helpers are installed or checked. | | Hindsight memory consolidation | live-gated: canary verifies strict config; actual migration/removal of competing memory systems remains a personal live operation. | | Plugin/MCP cleanup | live-gated: plan requires inventory and explicit local rollout before removing plugins, MCPs, or permissions. | From 057a36af367d2116055f2e0fabc327f9d34fe936 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 21:41:07 -0700 Subject: [PATCH 24/24] fix hook test shellcheck cleanup --- tests/test-hooks.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test-hooks.sh b/tests/test-hooks.sh index e051817..81d97f4 100755 --- a/tests/test-hooks.sh +++ b/tests/test-hooks.sh @@ -291,8 +291,10 @@ busy_ready="$TMPROOT/busy-port-ready" busy_error="$TMPROOT/busy-port-error" busy_pid="" cleanup_busy_port() { - [[ -n "$busy_pid" ]] && kill "$busy_pid" >/dev/null 2>&1 || true - [[ -n "$busy_pid" ]] && wait "$busy_pid" 2>/dev/null || true + if [[ -n "$busy_pid" ]]; then + kill "$busy_pid" >/dev/null 2>&1 || true + wait "$busy_pid" 2>/dev/null || true + fi rm -f -- "$busy_ready" "$busy_error" } trap 'cleanup_busy_port; cc_test_cleanup' EXIT