voidforge-build 23.12.2 → 23.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.claude/agents/leia-secrets.md +8 -0
- package/dist/.claude/agents/lucius-config.md +1 -0
- package/dist/.claude/commands/campaign.md +2 -1
- package/dist/.claude/commands/deploy.md +19 -1
- package/dist/CHANGELOG.md +53 -0
- package/dist/CLAUDE.md +1 -0
- package/dist/VERSION.md +3 -1
- package/dist/docs/methods/AI_INTELLIGENCE.md +3 -1
- package/dist/docs/methods/CAMPAIGN.md +6 -0
- package/dist/docs/methods/DEVOPS_ENGINEER.md +14 -2
- package/dist/docs/methods/GAUNTLET.md +6 -0
- package/dist/docs/methods/QA_ENGINEER.md +6 -0
- package/dist/docs/methods/SECURITY_AUDITOR.md +11 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +4 -0
- package/dist/docs/methods/TESTING.md +2 -0
- package/dist/docs/patterns/README.md +1 -0
- package/dist/docs/patterns/ai-prompt-safety.ts +55 -0
- package/dist/docs/patterns/codemod-hygiene.md +20 -0
- package/dist/docs/patterns/deploy-preflight.ts +53 -2
- package/package.json +2 -2
|
@@ -46,6 +46,14 @@ Rotation runbooks must name the exact dashboard path. User API Tokens live at My
|
|
|
46
46
|
- **Action:** Every secret rotation runbook MUST specify the exact dashboard path, not just the product name. Include both the User and Account paths when either could be the answer, and note which applies.
|
|
47
47
|
- **Scope:** SECRETS_MANAGEMENT.md, deploy runbooks, rotation verification scripts.
|
|
48
48
|
|
|
49
|
+
### Secrets hide in `.git/config` remote URLs, not just code/env
|
|
50
|
+
|
|
51
|
+
An HTTPS remote of the form `https://user:TOKEN@github.com/...` stores a live credential in plaintext in `.git/config` and prints it on every `git remote -v` — leaking into logs, CI output, and screen-shares. This surface lives outside the code/env/`.env` scope the secrets scan normally covers.
|
|
52
|
+
|
|
53
|
+
- **Evidence:** Field report #361 — a downstream session's first `git remote -v` printed a currently-valid GitHub PAT embedded in the `origin` URL.
|
|
54
|
+
- **Action:** Add a git-remote scan to every Phase-1 secrets pass: `git remote -v` plus `grep -E 'https://[^/@]+:[^@]+@' .git/config` (also `x-access-token:`/`oauth2:`). Flag matches CRITICAL; remediate by rotating the token and switching the remote to SSH or a credential helper.
|
|
55
|
+
- **Scope:** SECURITY_AUDITOR.md Phase 1, deploy-preflight, rotation runbooks.
|
|
56
|
+
|
|
49
57
|
## Reference
|
|
50
58
|
|
|
51
59
|
- Agent registry: `/docs/NAMING_REGISTRY.md`
|
|
@@ -43,6 +43,7 @@ Findings tagged by severity, with file and line references:
|
|
|
43
43
|
- Empty-string env defaults are a foot-gun: `${VAR:-}` (or any `VAR:-` shell/dotenv default) yields `""`, which is non-nullish — so `process.env.VAR ?? fallback` keeps the empty string and silently skips the fallback. Flag config that relies on nullish-coalescing defaults when the env layer can supply `""`; require explicit emptiness checks (`VAR || fallback`, or trim-and-test) at the boundary (field report #352, #5).
|
|
44
44
|
- Worker healthchecks must never hardcode dev hostnames (e.g. `localhost`, `127.0.0.1`, `*.local`): they pass in dev but false-fail in prod where the worker resolves a different host, marking healthy workers unhealthy and triggering needless restarts. Healthcheck targets belong in env/config, not source (field report #352, #5).
|
|
45
45
|
- Best-effort side effects (analytics, audit pings, cache warmups) must not be `await`ed on the auth path: awaiting a non-critical side effect blocks sign-in on its latency and turns its failure into a login failure. Fire-and-forget these (with their own error handling) so authentication completes independently (field report #352, #5).
|
|
46
|
+
- A strict validator on an *optional* env var crashes at boot on the empty string: `${VAR:-}` yields `""`, `.optional()` admits only `undefined` so `""` reaches `.url()`/`.email()`/enum and is rejected, throwing at config load. Flag any `z.string().url().optional()`-shaped schema on a var the env layer can supply as `""`; require `z.preprocess('' -> undefined)` ahead of the strict check (field report #356 #1).
|
|
46
47
|
|
|
47
48
|
## Reference
|
|
48
49
|
|
|
@@ -132,6 +132,7 @@ Read the PRD and diff against the codebase:
|
|
|
132
132
|
4. Read YAML frontmatter for skip flags (`auth: no`, `payments: none`, etc.)
|
|
133
133
|
5. **Classify every requirement by type:** Code (buildable), Asset (needs external generation — images, illustrations, OG cards), Copy (text accuracy), Infrastructure (DNS, env vars, dashboards)
|
|
134
134
|
6. Diff: what the PRD describes vs. what's implemented — **structural AND semantic** (not just "does the route exist?" but "does the component render what the PRD describes?")
|
|
135
|
+
6a. **Verify the premise before building (field report #360):** if a mission brief asserts a specific defect or its cause, confirm that IS the real problem in the code first — grep/read the named file and trace the actual failure path. A briefed "X is missing" may already exist (the real bug elsewhere); a briefed "friction" may be a CRITICAL dead-end. Re-scope to the verified root cause if the premise is wrong.
|
|
135
136
|
7. Produce the ordered mission list — each mission is 1-3 PRD sections, scoped to be buildable in one `/assemble` run
|
|
136
137
|
8. **Pike** (`subagent_type: Pike`) **challenges the ordering:** "Should we attempt a harder mission first while context is fresh?" Bold counterbalance to Dax's dependency-based ordering. If Pike's argument is stronger, reorder.
|
|
137
138
|
9. **Separately list BLOCKED items** — asset/infrastructure requirements that code can't satisfy
|
|
@@ -216,7 +217,7 @@ After `/assemble` completes:
|
|
|
216
217
|
|
|
217
218
|
All PRD requirements are COMPLETE or explicitly BLOCKED:
|
|
218
219
|
|
|
219
|
-
1. **Run `/gauntlet` (full 5 rounds)** — mandatory final Gauntlet on the complete codebase. This is non-negotiable, even with `--fast`. The Gauntlet tests the combined system across all domains: architecture, code review, UX, security, QA, DevOps, adversarial crossfire, and council convergence. Individual `/assemble` runs review one mission at a time; the Gauntlet reviews everything together.
|
|
220
|
+
1. **Run `/gauntlet` (full 5 rounds)** — mandatory final Gauntlet on the complete codebase. This is non-negotiable, even with `--fast`. The Gauntlet tests the combined system across all domains: architecture, code review, UX, security, QA, DevOps, adversarial crossfire, and council convergence. Individual `/assemble` runs review one mission at a time; the Gauntlet reviews everything together. The Victory Gauntlet MUST include the composition/wiring lens (see GAUNTLET.md 'Composition/wiring lens'): one agent reconciles every assembled entry point's actual arguments/config against the library's contract and the safe defaults. Per-mission reviews cannot catch cross-mission composition gaps — this lens is why the final Gauntlet is non-negotiable.
|
|
220
221
|
2. **Fix all Critical and High findings** from the Gauntlet.
|
|
221
222
|
3. **Troi** (`subagent_type: Troi`) **reads the PRD section-by-section** (runs as part of the Gauntlet Council round) — verifies every prose claim against the implementation. Not just "does the route exist?" but "does the component render what the PRD describes?" Checks numeric claims, visual treatments, copy accuracy, asset gaps.
|
|
222
223
|
4. Fix code discrepancies. Flag asset requirements as BLOCKED.
|
|
@@ -36,7 +36,9 @@ Levi verifies the deploy is safe:
|
|
|
36
36
|
3. **No uncommitted changes:** `git status` clean
|
|
37
37
|
4. **Credentials available:** SSH key, API token, or platform credentials accessible
|
|
38
38
|
5. **Version tagged:** Current version from VERSION.md matches the commit being deployed
|
|
39
|
-
6.
|
|
39
|
+
6. **Config loads under prod env:** run the app's config validator (not just `docker compose config`, which only renders). `compose config` resolves env but does not run app-level Zod/schema validation — an optional strict-validated var fed `""` by `${VAR:-}` renders clean yet throws at boot. Run the config loader (or canary the worker — see Step 3) before the serving container goes live. (Field report #356)
|
|
40
|
+
7. **Mandatory adversarial review for untrusted-data -> user-facing-sink changes:** If this deploy introduces a new path from untrusted data (extracted/user/third-party URL or text) to a user-facing sink (event body, email, SMS, push, chat receipt, webhook), the adversarial security review (Kenobi: Maul + Windu open-redirect/link-injection/sink-egress checks per SECURITY_AUDITOR.md "Mandatory Adversarial Review") MUST have run and passed before deploy. This is NOT author discretion. ABORT if it has not run. (Field report #359: a new untrusted `conference_url` would have shipped a High open-redirect into Calendar + Telegram/Slack/email receipts; the review caught it.)
|
|
41
|
+
8. If any check fails → ABORT with clear error message
|
|
40
42
|
|
|
41
43
|
## Step 2.5 — Pre-Deploy Secret Scan (Leia)
|
|
42
44
|
|
|
@@ -53,6 +55,14 @@ ANY hit aborts the deploy with a non-zero exit and prints the offending path(s).
|
|
|
53
55
|
|
|
54
56
|
Evidence: field report #305 — 32-day live credential leak caused by `.env` in deploy payload. Pre-deploy scan would have caught it on the first deploy.
|
|
55
57
|
|
|
58
|
+
## Step 2.6 — Pre-Build Disk Preflight (Mustang)
|
|
59
|
+
|
|
60
|
+
For single-host Docker/VPS targets, before `docker build`, run the Pre-Build Disk Preflight (DEVOPS_ENGINEER.md): if free space is below threshold, prune build cache + stale SHA-tagged images (preserving the rollback tag) before building. A build that fails at image export wastes the full npm ci + build. (Field report #357 #1.)
|
|
61
|
+
|
|
62
|
+
## Step 2.7 — Prompt-Change Eval Gate (Bayta) — when the deploy includes an eval-tracked prompt change
|
|
63
|
+
|
|
64
|
+
If this deploy touches any eval-tracked prompt (extraction/classification/generation prompt with a golden dataset), the LIVE eval MUST have run and passed IN THIS SESSION before deploy — it is the agent's job, not a deferral to the operator. Run the secret-injected runner the repo provides (e.g. `npm run eval:op`, which wraps the eval in `op run --env-file=op/eval.env.op -- ...` so 1Password injects the model key) rather than treating `npm run eval` as an operator-only step. A prompt change is NOT deploy-ready until its LIVE eval is green. ABORT if the eval has not run or is red. (Field report #359: a deferred eval would have shipped an `is_virtual` 1.00->0.00 regression; running it inline caught it.)
|
|
65
|
+
|
|
56
66
|
## Step 3 — Deploy Execution (Levi)
|
|
57
67
|
|
|
58
68
|
Execute the deploy strategy for the detected target:
|
|
@@ -71,6 +81,12 @@ Execute the deploy strategy for the detected target:
|
|
|
71
81
|
**Docker:** `docker build -t app . && docker push && ssh ... "docker pull && docker restart"`
|
|
72
82
|
**Static/Cloudflare:** `wrangler deploy` or S3 sync
|
|
73
83
|
|
|
84
|
+
**Config-affecting change? Canary the worker first.** When the deploy changes env/config that BOTH web and workers load, deploy the worker (or one worker replica) FIRST and confirm it boots clean. The worker loads the same config a strict validator would crash on, but a worker crash does not pull the serving web container out of rotation — so a config boot-crash (see Step 2 item 6 and §Config Foot-Guns: empty-string-into-strict-Zod) is caught on the worker without taking the site down. Only after the worker is healthy do you reload/restart web. (Field report #356 #2.)
|
|
85
|
+
|
|
86
|
+
## Step 3.5 — Pre-Prod Verification Strategy
|
|
87
|
+
|
|
88
|
+
If there is no staging environment AND the product is low-traffic/pre-real-users AND rollback is fast, prefer a canary deploy + verify-on-prod (rollback armed) over a localhost simulation; see CAMPAIGN.md Pre-Prod Verification. (Field report #357 #2.)
|
|
89
|
+
|
|
74
90
|
## Step 4 — Health Check (L)
|
|
75
91
|
|
|
76
92
|
After deploy completes:
|
|
@@ -122,6 +138,8 @@ This gate is mandatory and non-skippable for any deploy that serves a built fron
|
|
|
122
138
|
|
|
123
139
|
## Step 5 — Rollback (Valkyrie)
|
|
124
140
|
|
|
141
|
+
Before rolling back on a failed OAuth sign-in, check whether the error is on the IdP domain (pre-callback) vs your callback — an IdP-side error with a re-auth token is usually transient; retry incognito first. (Field report #357 #3; see DEVOPS_ENGINEER.md Deploy Safety Rules.)
|
|
142
|
+
|
|
125
143
|
If health check fails:
|
|
126
144
|
1. **VPS:** `ssh ... "git checkout HEAD~1 && npm ci && npm run build && pm2 restart"`
|
|
127
145
|
2. **Vercel:** `vercel rollback --token $VERCEL_TOKEN`
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,59 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [23.13.1] - 2026-06-12
|
|
10
|
+
|
|
11
|
+
### Publish-gate fix for v23.13.0 (stale surfer-gate test)
|
|
12
|
+
|
|
13
|
+
v23.13.0 was committed, tagged, and pushed but **never published to npm** — the `Publish to npm` workflow's `test` stage failed before any publish job ran (npm stayed at 23.12.2; no partial release).
|
|
14
|
+
|
|
15
|
+
Cause: the #360 roster-TTL change raised `ROSTER_TTL_SECONDS` 600 → 3600 in `scripts/surfer-gate/check.sh`, but the gate's own `test.sh` "Stale roster (>10min) blocks" case ages a roster **11 minutes** and asserts a block (exit 2). Under the new 1-hour TTL an 11-minute-old roster is still *fresh*, so the gate correctly returned exit 0 — and the test (asserting 2) failed, tripping the CI `pretest` gate.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **`scripts/surfer-gate/test.sh`** (+ tracked `packages/methodology/` mirror) — age the stale-roster test fixture to **61 minutes** (past the new 3600s TTL) and relabel the case ">1hr". Gate suite back to 20/20; full workspace suite 1390/1390. No behavior change beyond v23.13.0.
|
|
20
|
+
|
|
21
|
+
### Lesson
|
|
22
|
+
|
|
23
|
+
A threshold/TTL change in a gate script must update that gate's adversarial test **in the same commit** — the stale-roster assertion is exactly the threshold-coupled test that #356-F4 (reproduce through the real path) and #358-F1 (composition gaps) warn about. Dep range `^23.13.0` → `^23.13.1` (ADR-062).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## [23.13.0] - 2026-06-12
|
|
28
|
+
|
|
29
|
+
### Field Report Triage — 6 reports closed (#356–#361)
|
|
30
|
+
|
|
31
|
+
`/debrief --inbox` triaged all six open field reports against the post-v23.12.2 tree via two-phase workflow orchestration — per-report investigators classified each proposed fix (accept / already-fixed / wontfix / needs-info) with file-quoted evidence, an adversarial pass independently re-verified every `already-fixed` verdict, then per-file appliers landed the accepted edits. 25 proposed fixes → **23 accepted, 1 already-fixed (verified), 1 wontfix**. Applied across 17 files + 1 new pattern. Five clusters:
|
|
32
|
+
|
|
33
|
+
- **Deploy safety** — the empty-string-into-strict-Zod boot-crash trap (`${VAR:-}` → `""` defeats `z.string().url().optional()` because `.optional()` only admits `undefined`; fix is `z.preprocess('' → undefined)` ahead of the strict check), "render is not load" (`docker compose config` resolves env but never runs the app's config validator — verify config LOADS), canary-the-worker-first on config-affecting changes, pre-build disk preflight (prune cache + stale SHA tags, keep the rollback tag), and OAuth post-deploy IdP-side-vs-regression discrimination (don't reflexively roll back on an IdP-domain error — retry incognito). (#356, #357)
|
|
34
|
+
- **Adversarial-verify rigor** — a CONFIRM backed by "I reproduced it" counts only when reproduced through the REAL execution path (the actual CLI/tool/runtime), not the underlying library in isolation (#356); the Victory Gauntlet MUST include a composition/wiring lens over the assembled entry paths (per-mission reviews are structurally blind to cross-mission composition), and a conditional "safe to ship gated-off but not to arm" verdict requires a ship-vs-enable ADR + prerequisites runbook before sign-off (#358).
|
|
35
|
+
- **Mandatory verification** — prompt evals run INLINE via the secret-injected runner (`npm run eval:op`), not deferred to the operator (#359); the adversarial security review is REQUIRED (not author-discretionary) for any change adding an untrusted-data → user-facing-sink path (#359); live-fire every external credential against its provider before marking it done — env-var-set ≠ done (#360); and verify a mission brief's premise against the code before scoping the fix (#360).
|
|
36
|
+
- **Secret surfaces** — git remote / `.git/config` inline-credential scan added to Kenobi/Leia Phase 1, the deploy-preflight pattern, and DEVOPS deploy-safety rules; a live PAT in an HTTPS remote URL was invisible to every prior secrets check. (#361)
|
|
37
|
+
- **Test fidelity** — real-output seeded-mutant self-test (does-it-fix / does-no-harm) mandated for any LLM/external-output boundary; "if every test of an integration boundary uses a fixture you authored, you have not tested the boundary." (#358)
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`docs/patterns/codemod-hygiene.md`** (52nd pattern) — after a jscodeshift/recast/`@next/codemod` run, strip incidental reformatting (recast re-prints touched nodes) so the diff shows only the semantic change. Registered in `docs/patterns/README.md` and the CLAUDE.md Code Patterns list. (#357)
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **`docs/methods/DEVOPS_ENGINEER.md`** — Config Foot-Guns 4th trap (strict-validated optional env boot-crash + `z.preprocess` fix); "render is not load" compose sub-bullet (count Two→Three); Pre-Build Disk Preflight subsection; live-fire-per-credential and OAuth-IdP-side deploy-safety rules. (#356, #357, #360)
|
|
46
|
+
- **`.claude/commands/deploy.md`** — Step 2 pre-deploy items: config-loads check (#356) + mandatory untrusted→sink review (#359); new Step 2.6 disk preflight (#357) and Step 2.7 prompt-eval gate (#359); canary-worker-first in Step 3, Step 3.5 pre-prod verification strategy (#357), Step 5 rollback IdP-side preamble (#357).
|
|
47
|
+
- **`docs/methods/GAUNTLET.md`** — reproduce-through-real-execution-path verify rule (#356); composition/wiring lens + ship-vs-enable conditional-verdict requirement (#358).
|
|
48
|
+
- **`docs/methods/CAMPAIGN.md` + `.claude/commands/campaign.md`** — premise-verification sub-step (#360); pre-prod-verification-when-no-staging branch + dependency-feasibility-first reference (#357); Victory Gauntlet composition-lens cross-reference (#358).
|
|
49
|
+
- **`docs/methods/SECURITY_AUDITOR.md`** — Phase-1 git-remote credential scan (#361); mandatory untrusted-data→user-facing-sink adversarial-review trigger (#359).
|
|
50
|
+
- **`docs/methods/QA_ENGINEER.md` + `docs/methods/TESTING.md`** — real-output seeded-mutant self-test for LLM/external-output boundaries (#358); seed-draft + `?draft=<id>` deep-link screenshot technique when the worker pipeline is down (#359).
|
|
51
|
+
- **`docs/methods/AI_INTELLIGENCE.md`** — the LIVE eval is run by the agent in-session via the secret-injected `eval:op` runner, never deferred to the operator (LIVE-eval-gate subsection + Operating Rule 5) (#359); **`docs/methods/SYSTEMS_ARCHITECT.md`** — Dependency-Feasibility-First migration gate (#357).
|
|
52
|
+
- **`docs/patterns/ai-prompt-safety.ts`** — lenient-schema + sanitize-at-trusted-boundary pattern for untrusted extraction fields (#359); **`docs/patterns/deploy-preflight.ts`** — opt-in `.git/config` inline-credential scan, never prints the matched secret (#361).
|
|
53
|
+
- **Agents** — `lucius-config.md` (strict-validator-on-optional-env learning, #356) and `leia-secrets.md` (`.git/config` remote-URL secret learning, #361) Operational Learnings.
|
|
54
|
+
- **`scripts/surfer-gate/check.sh` + `docs/adrs/ADR-060`** — roster TTL 600s → 3600s with mtime refresh-on-activity, so long real-code missions don't force redundant Surfer re-scans mid-mission. (#360)
|
|
55
|
+
|
|
56
|
+
### Pipeline
|
|
57
|
+
|
|
58
|
+
Cut via two background workflows (investigate→verify, then per-file apply) with a full `git diff` review gate before commit. **#358-F3** (find→verify ≥2/3 adversarial-lens pattern) verified already-shipped in v23.12.0 (`SUB_AGENTS.md`) — no change. **#360-F4** (don't pin a sunsetting external-API version without a health check) reporter-scoped to project LEARNINGS; its kernel is folded into the #360 live-fire-per-credential rule. Dep range `^23.12.2` → `^23.13.0` (ADR-062). Tracked generated copies re-synced: `packages/methodology/CLAUDE.md` (ADR-058 strip) and `packages/methodology/scripts/surfer-gate/check.sh`.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
9
62
|
## [23.12.2] - 2026-06-09
|
|
10
63
|
|
|
11
64
|
### `/git` monorepo release-discipline fix
|
package/dist/CLAUDE.md
CHANGED
|
@@ -129,6 +129,7 @@ Reference implementations in `/docs/patterns/`. Match these shapes when writing.
|
|
|
129
129
|
- `design-tokens.ts` — Semantic color/type tokens (one indirection layer) so a theme pivot is a token change, not a component-wide find-replace (field report #351, #343)
|
|
130
130
|
- `nginx-vhost.conf` — Cloudflare-Flexible-safe vhost template: security headers, ACME http-01 passthrough, no redirect loop behind CF's flexible SSL (field report #351, #344)
|
|
131
131
|
- `error-message-categorization.tsx` — Categorize errors at the UI boundary (network / auth / validation / server / unknown) before choosing copy, so users see actionable messages not raw internals (field report #351, #343)
|
|
132
|
+
- `codemod-hygiene.md` — after a jscodeshift/recast codemod, strip incidental reformatting so the diff shows only the semantic change (field report #357)
|
|
132
133
|
|
|
133
134
|
## Slash Commands
|
|
134
135
|
|
package/dist/VERSION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Version
|
|
2
2
|
|
|
3
|
-
**Current:** 23.
|
|
3
|
+
**Current:** 23.13.1
|
|
4
4
|
|
|
5
5
|
## Versioning Scheme
|
|
6
6
|
|
|
@@ -14,6 +14,8 @@ This project uses [Semantic Versioning](https://semver.org/):
|
|
|
14
14
|
|
|
15
15
|
| Version | Date | Summary |
|
|
16
16
|
|---------|------|---------|
|
|
17
|
+
| 23.13.1 | 2026-06-12 | Publish-gate fix for v23.13.0. The #360 roster-TTL change (600s→3600s in `scripts/surfer-gate/check.sh`) did not update the gate's own `test.sh`, whose "Stale roster (>10min) blocks" case aged a roster 11 min and expected a block — now still *fresh* under the 1-hour TTL, so it returned exit 0 (expected 2). The CI `pretest` gate (`bash scripts/surfer-gate/test.sh`) failed → the `Publish to npm` job's test stage failed → both publish jobs were skipped (v23.13.0 was tagged but **never published**; npm stayed at 23.12.2). Fix: age the stale-roster test roster to 61 min (past the new TTL) and relabel ">1hr". Pure CI-gate fix — no methodology behavior change beyond v23.13.0. Full suite 1390/1390 green. Lesson for next time: a TTL/threshold change in a gate script must update the gate's adversarial test in the same commit (the test.sh stale case is exactly the kind of threshold-coupled assertion #356-F4 / #358-F1 warn about). Dep range `^23.13.0` → `^23.13.1` (ADR-062). |
|
|
18
|
+
| 23.13.0 | 2026-06-12 | Field Report Triage — 6 reports closed (#356–#361). `/debrief --inbox` triaged all 6 open reports against the post-v23.12.2 tree via two-phase workflow orchestration (per-report investigators → adversarial verify of every already-fixed verdict → per-file appliers), applying 23 accepted fixes across 17 files + 1 new pattern. Clusters: **deploy-safety** (empty-string-into-strict-Zod boot crash + `z.preprocess` fix, "render≠load" config-LOADS gate, canary-worker-first, pre-build disk preflight, OAuth IdP-side-vs-regression — DEVOPS_ENGINEER.md, deploy.md, lucius-config); **adversarial-verify rigor** (reproduce through the REAL execution path not a library-in-isolation, GAUNTLET.md #356; composition/wiring lens for the Victory Gauntlet + ship-vs-enable ADR requirement #358); **mandatory-verification** (run prompt evals INLINE via `eval:op` not deferred to operator + mandatory adversarial review for untrusted→user-facing-sink paths #359; live-fire credential verification + premise-verification recon #360); **secret surfaces** (git remote / `.git/config` inline-credential scan — SECURITY_AUDITOR.md Phase 1, leia-secrets, deploy-preflight.ts, DEVOPS #361); **test fidelity** (real-output seeded-mutant self-test for LLM/external-output boundaries — QA_ENGINEER.md/TESTING.md #358). Surfer-gate roster TTL 600s→3600s + refresh-on-activity (check.sh/ADR-060 #360). New pattern `codemod-hygiene.md` (strip incidental recast reformatting #357; 51→52). #358-F3 (find→verify pattern) verified already-shipped in v23.12.0; #360-F4 reporter-scoped to project LEARNINGS. Dep range `^23.12.2` → `^23.13.0` (ADR-062). |
|
|
17
19
|
| 23.12.2 | 2026-06-09 | `/git` monorepo release-discipline fix. The `/git` command's version-bump steps (3–5) assumed a single `package.json` and would have under-bumped this monorepo — missing the second workspace package and the ADR-062 dep pin (both bumped by hand in v23.12.0/.1). `git.md` Step 3 now bumps **every** versioned `package.json` + the `voidforge-build-methodology` dep pin + re-syncs the tracked `packages/methodology/CLAUDE.md` generated copy; Steps 4/5 staging+verify updated to match. `RELEASE_MANAGER.md` gains two troubleshooting rules paid for this session: **E404-on-publish = wrong npm account/scope, not expiry** (check `npm owner ls` first; in CI it's the `NPM_TOKEN` secret's account — cites the four-failed-runs incident where a rotated token was from a non-owner account) and **sequential oldest-first multi-version publish** so `latest` lands on the newest semver. First release cut via the corrected procedure (dogfood). Dep range `^23.12.1` → `^23.12.2`. |
|
|
18
20
|
| 23.12.1 | 2026-06-09 | Follow-on field-report pass — `/debrief --inbox` triaged #354/#355 (filed during the v23.12.0 run) against the post-v23.12.0 tree and applied 8 fixes across 15 files. #354: port the vote-based REFUTE lens from /gauntlet into `/engage` + `/sentinel` (they used the old "second agent disagrees → drop" model), name find→cluster→3-lens-verify as the default review shape in `SUB_AGENTS.md`, **enforcement-keyed severity rubric** (a server-enforced client affordance leak is UX P2/P3, not a P0 breach — `SECURITY_AUDITOR.md`/`PRODUCT_DESIGN_FRONTEND.md`/`/ux`/`/sentinel`), "isolation-green ≠ deploy-green" (`BUILD_PROTOCOL.md`/`/deploy`/`QA_ENGINEER.md`), boot-time DDL-ownership class (`DEVOPS_ENGINEER.md`/`database-migration.ts`). #355: **contrast findings must cite literal source hex for fg+bg with file:line + re-grep the pairing before Critical** (token NAMES ≠ VALUES — defends against the false site-wide Critical; `PRODUCT_DESIGN_FRONTEND.md`/`GAUNTLET.md`/Samwise), **glob-derived fan-out work-lists + mandatory post-fan-out residual sweep** (`CAMPAIGN.md`/`SUB_AGENTS.md`/herald), focused single-lens roster cap + surface-partition (herald/`/ux`/`GAUNTLET.md`), per-wave staging deploy = status checkpoint inlined in CAMPAIGN action prose (verify pass overturned a false already-fixed). #355 F5 confirmed already-shipped (derived-counts doctrine). Also: **fixed the chronically-red `validate-branches.yml` slash-command CI check** (its grep mis-read `/docs/*` Docs-Reference rows as commands — the #352 "gate that doesn't gate" class) and registered `/audit-docs` in the CLAUDE.md Slash Commands table. Dep range `^23.12.0` → `^23.12.1`. |
|
|
19
21
|
| 23.12.0 | 2026-06-09 | The v23.12 methodology pass — `/debrief --inbox` triaged all 12 open field reports (#342–#353) and applied every accepted fix in one session via two-phase workflow orchestration (triage → apply), with an adversarial verify pass on every file. 58 fixes across 32 files + 5 new files. 7 clusters: **verify-the-FIX** (the adversarial pass must vet the proposed fix, not just the finding — SUB_AGENTS.md, GAUNTLET.md, /engage; #348/#349/#350, M5 mint-fence incident); **production-config gate** (sandbox-green ≠ ship-ready — GAUNTLET.md prod-boot + sandbox-blind-spot round, CAMPAIGN.md Victory Checklist; #350); **Spring Cleaning consumer-vs-clone** (FORGE_KEEPER.md destructive-risk branch so app projects don't lose tsconfig/lockfiles; #343 F10); **Surfer roster sizing** (silver-surfer-herald.md scope_bias/scope_density/~18-cap + basename normalization; #343/#344/#345/#346); **creative/UX grounding** (world-scan + de-AI + token-scoped theming — ux.md, PRODUCT_DESIGN_FRONTEND.md, galadriel; #347/#351); **deploy/DevOps foot-guns** (DEVOPS_ENGINEER.md +13: eval-env, Node-MDWE, CF-Flexible, served-vs-built, compose-topology, docker-cleanup; #344/#349/#352/#353); **doc-currency** (CAMPAIGN/ASSEMBLER pre-SEAL refresh + new /audit-docs & DOC_AUDIT.md; #342). 3 new patterns (design-tokens.ts, nginx-vhost.conf, error-message-categorization.tsx; 48 → 51) + new /audit-docs command + DOC_AUDIT.md + scripts/regen-claude-md.sh. CLAUDE.md Personality +2 (anti-picker #343, authorized-autonomy #344), gate-timing #348, roster normalization #345. Dep range `^23.11.4` → `^23.12.0` (ADR-062). #349 F-4 and #352 #3 were already shipped (verified); #345 DEAL-004 + #353 RC-001/002/callout out of scope (Claude Code core / Workflow tool). |
|
|
@@ -45,7 +45,7 @@ The metaphor is precise. Psychohistory predicts outcomes from patterns, adapts w
|
|
|
45
45
|
2. **Every AI call must have a fallback path.** The application must function when the model fails.
|
|
46
46
|
3. **Token usage must be tracked and bounded.** Unbounded token spend is a billing incident.
|
|
47
47
|
4. **Model selection must be justified.** "We used Opus because it's the best" is not a justification. Match capability to task.
|
|
48
|
-
5. **Evaluation must exist before shipping.** If you can't measure whether the output is correct, you can't ship it.
|
|
48
|
+
5. **Evaluation must exist before shipping.** If you can't measure whether the output is correct, you can't ship it. The eval is run by the agent in-session via the secret-injected runner (e.g. `npm run eval:op`) — never deferred as an "operator step" when the repo provides one (field report #359).
|
|
49
49
|
6. **Safety review must happen before user-facing AI.** Prompt injection is the new SQL injection.
|
|
50
50
|
7. **Observability is not optional.** You must be able to see what the AI decided and why.
|
|
51
51
|
8. **Context windows are finite.** Design for it. Don't assume infinite context.
|
|
@@ -162,6 +162,8 @@ Evals stratify into two layers, and they catch different bug classes:
|
|
|
162
162
|
|
|
163
163
|
Treat the LIVE layer as a mandatory gate, not an optional smoke test: a component cannot ship until its LIVE eval has run against the real model and passed. The sandbox layer gates *every commit*; the LIVE layer gates *every launch*.
|
|
164
164
|
|
|
165
|
+
**The agent runs the LIVE eval in-session — it is not "owed to the operator" (field report #359).** A common false deferral: the author bumps an eval-tracked prompt and flags it as "needs the operator's Anthropic key before prod." That deferral is wrong when the repo ships a secret-injected eval runner. If the project exposes `npm run eval:op` (which wraps the eval in `op run --env-file=op/eval.env.op -- ...` so 1Password injects the key), the LIVE eval is RUNNABLE BY THE AGENT THIS SESSION — run it before declaring the prompt change done. A prompt change is not *done* until its LIVE eval has been run and is green; running it is the agent's job, not a handoff. The eval-prompts gate's own failure message must point at the op-injected runner (`npm run eval:op`), not a bare `npm run eval` that implies an operator-only step. Deferring the eval ships exactly the regression the gate exists to catch — field report #359 deferred it and would have shipped an `is_virtual` 1.00→0.00 regression that running it inline immediately caught.
|
|
166
|
+
|
|
165
167
|
**Gotcha — normalize null-to-undefined before Zod `.optional()` (field report #352, #4).** A live model emits `null` (not omission) for an absent optional field — e.g. it returns `{ "category": "billing", "subcategory": null }` rather than dropping `subcategory`. Zod's `.optional()` accepts `undefined`, **not** `null`, so the valid response fails schema validation and your retry/fallback path fires on output that was actually fine. This is invisible in the sandbox layer because hand-authored fixtures usually omit the key instead of setting it to `null`. Normalize before validating:
|
|
166
168
|
|
|
167
169
|
```ts
|
|
@@ -219,6 +219,7 @@ Dax reads the Prophets' plan:
|
|
|
219
219
|
- **Vault-Available** — infrastructure items where credentials exist in `~/.voidforge/vault.enc` but haven't been injected into `.env`. When scanning `.env.example` against `.env`, check if missing vars are in the vault before marking BLOCKED. Vault-backed credentials can be auto-resolved by running `voidforge deploy`. (Field report #40: 5 items classified as BLOCKED for an entire 10-mission campaign when the vault had the credentials.)
|
|
220
220
|
- **Content Audit** — verify marketing claims, feature descriptions, and documentation against the actual codebase. Run after major version changes when copy may have drifted from implementation. Maps to FIELD_MEDIC.md "Marketing drift" root cause. (Field report #243)
|
|
221
221
|
7. Diff: PRD requirements vs. implemented features (structural AND semantic — not just "does the route exist?" but "does the component render what the PRD describes?")
|
|
222
|
+
7a. **Premise verification (field report #360).** For any mission whose brief asserts a specific defect, gap, or cause — "endpoint X is missing," "flow Y has friction," "bug is in module Z" — confirm the stated problem IS the actual problem in the code BEFORE scoping the fix. Grep/read the named artifact and trace the real failure path. A brief's framing is a hypothesis, not a finding. Three failure modes to catch: (a) the thing said to be missing already exists and the real bug is elsewhere (a briefed "resend endpoint missing" was actually a session-gating deadlock — unverified users couldn't get a session to reach the resend button), (b) the mechanism is mis-stated, (c) the briefed "minor friction" is actually a CRITICAL dead-end. If the premise is wrong, re-scope the mission to the verified root cause and note the correction in the mission brief — do not build the briefed fix on an unverified premise.
|
|
222
223
|
8. Produce: **The Prophecy Board** — ordered list of missions with scope, plus a separate list of BLOCKED items (assets, credentials, user decisions)
|
|
223
224
|
8a. **Cross-mission data handoff check (Odo):** For any system that forms a closed loop (e.g., generate → track → analyze → feed back), identify every data handoff point between missions. Each handoff must be explicitly scoped in at least one mission: "Mission N produces X, Mission M consumes X via [mechanism]." If the loop spans 3+ missions, draw the handoff map. Unscoped handoffs become no-ops — the code on each side compiles and tests independently, but the data never flows between them. (Field report #265: seedPush extracted winning variant data but discarded it — the feedback loop was documented but not wired because the two ends were in separate missions with no explicit handoff.)
|
|
224
225
|
9. **Cluster-mission recognition:** Before finalizing the board, Dax asks: "Are any of these missions cluster-natured?" A cluster-mission is a single-line entry that actually spans 4+ ADR sections, 4+ sub-components, or 4+ migration steps. Examples: M-51 cluster (per-org MCP topology) genuinely required 4 sub-missions per ADR-107 §c-§f; M-44 series required 5 sub-missions per ADR-117. Pretending a cluster is one mission produces 2-3× planning underestimates and forces mid-campaign restructuring. If a mission has 4+ named deliverables in different files/modules, split into sub-missions (M-51a/b/c/d) at plan time, not at execution time. (Field report #326: Sisko's original v7.10 slate was 9 missions; reality was 21 because cluster recognition was deferred.)
|
|
@@ -268,6 +269,7 @@ Before starting mission #1, Odo verifies:
|
|
|
268
269
|
3. Are new integrations needed that require credentials?
|
|
269
270
|
4. Are there blocking issues from previous missions?
|
|
270
271
|
5. **Data model retrofit check:** If this campaign adds a new data model layer (e.g., ProjectVersion, WorkspaceScope), identify all existing endpoints that read/write the old model and flag them for review. Prior-campaign features that reference the old model directly will silently break or return stale data. (Field report #38: variant endpoint missed the version model because it was built in a prior campaign.)
|
|
272
|
+
6. **Dependency-Feasibility-First (framework/major-version migrations):** For framework/major-version migration missions, run the Dependency-Feasibility-First gate (SYSTEMS_ARCHITECT.md) before plan finalization — if a required peer has no version supporting the target framework, the mission is BLOCKED upstream, not buildable. (Field report #357.)
|
|
271
273
|
|
|
272
274
|
**BLOCKED Validation Rule:** Before declaring a mission BLOCKED, verify the block is real. If credentials exist in .env or vault, attempt the API call. "Needs dashboard access" is NOT a valid blocker if an API endpoint exists. "Needs developer account" is NOT valid if the API is publicly documented and callable with `node:https`. Try before blocking.
|
|
273
275
|
|
|
@@ -304,6 +306,10 @@ User confirms, redirects, or overrides. On confirm → Step 4.
|
|
|
304
306
|
|
|
305
307
|
**Silver Surfer gate fires at the REVIEW phase, not the solo build.** Within a mission, the gate (ADR-051 PreToolUse hook on the Agent tool) engages when Fury deploys the review/audit roster as sub-agents — NOT during the orchestrator's solo build of the mission's code. Solo-build-before-review is intentional, not a skipped gate: parallel agents editing the same tightly-coupled engine files (game loop, state machine, shared service) would clobber each other's edits and produce merge garbage. So the orchestrator builds the changeset solo, THEN the Surfer-gated review roster reads it. If you find yourself mid-build asking "did a gate get skipped?", the answer is no — the gate has not fired yet because the review phase has not started. (Field report #348 #3: mid-build confusion over an un-fired gate that fires correctly at the review phase.)
|
|
306
308
|
|
|
309
|
+
### Pre-Prod Verification: when there is no staging
|
|
310
|
+
|
|
311
|
+
Verify-in-non-prod-before-prod is the default, not an absolute. When (a) no staging/preview environment exists, AND (b) the product is low-traffic or pre-real-users, AND (c) rollback is fast (single command, previous image/commit armed), prefer a canary deploy + verify-on-prod-with-rollback-armed over a contrived non-prod simulation. A localhost OAuth sim — throwaway redirect URI, prod creds injected, host-pin overridden — tests a fake environment, not the real one; for the no-staging + low-blast + fast-rollback case it is strictly worse than testing the real thing in the real place with rollback one command away. Do NOT mandate a localhost sim when canary+verify-on-prod is the higher-fidelity, lower-friction proxy. Reserve the non-prod requirement for products with real users where a bad prod request is itself the harm. (Field report #357 #2: operator pushed back on localhost theater for a live, pre-real-users product — correctly.)
|
|
312
|
+
|
|
307
313
|
**Dispatch model (ADR-044):** Per-mission `/assemble` runs SHOULD dispatch phases to sub-agents per `SUB_AGENTS.md` "Parallel Agent Standard." Agents are launched as named subagent types defined in `.claude/agents/` with description-driven dispatch — Opus scans `git diff --stat` and matches changed files against agent descriptions to auto-select specialists. The campaign orchestrator (main thread) manages the mission sequence, inter-mission gates, and campaign state — it does NOT perform inline code analysis. Pass findings summaries between missions, not raw code. See `docs/AGENT_CLASSIFICATION.md` for the full agent manifest (see docs/AGENT_CLASSIFICATION.md). (Field report #270)
|
|
308
314
|
|
|
309
315
|
### Campaign-Mode Pipeline
|
|
@@ -97,10 +97,11 @@ For each service in `docker-compose.yml`, verify:
|
|
|
97
97
|
7. **Dependency health** — `depends_on` with `condition: service_healthy` (compose v2.1+). Without it, the app starts before its database is ready.
|
|
98
98
|
(Field report #280)
|
|
99
99
|
|
|
100
|
-
**Compose validation goes deeper than syntax (field report #352 #2).** `docker compose config` only validates *syntax* — it renders the merged YAML and exits 0 even when the resulting topology is wrong.
|
|
100
|
+
**Compose validation goes deeper than syntax (field report #352 #2).** `docker compose config` only validates *syntax* — it renders the merged YAML and exits 0 even when the resulting topology is wrong. Three failure modes it will not catch:
|
|
101
101
|
|
|
102
102
|
- **Dependency closure.** A service can reference a network, volume, or `depends_on` target whose definition exists but whose *startup* chain is broken. Check the closure with `docker compose up --dry-run` — it walks the full dependency graph and reports what would actually start (and in what order) without launching containers.
|
|
103
103
|
- **Overlay merge, not overlay replace.** Compose **merges** list-and-map fields like `depends_on` and `environment` across overlay files (`-f base.yml -f docker-compose.dev.yml`); it does not replace them. The classic trap: `base.yml` declares `depends_on: [redis]` for development, and an overlay tries to drop it with `depends_on: []` — the empty list **merges into** the base list, the `redis` edge **survives**, and prod still waits on (or starts) a dev-only Redis. To *replace* rather than merge, use the override tags: `depends_on: !override []` (replace the whole list) or `!reset null` (remove the key entirely). Verify the rendered result with `docker compose config` and confirm the unwanted edge is actually gone — never assume the overlay won.
|
|
104
|
+
- **Render is not load.** `docker compose config` resolves env values into the rendered YAML but never executes the app's own runtime config validation (Zod/envalid/pydantic). A schema that throws on a value compose happily renders — the empty-string-into-`.url()` crash above (§Config Foot-Guns, field report #356) — exits 0 under `compose config` and only surfaces at container boot. Before deploying a config-affecting change, run the app's config loader against the prod env (`node -e "require('./dist/config')"`, `python -c "import app.config"`, or a dedicated `npm run config:check`), OR canary the config-loading worker (§Deploy sequence) so a boot crash surfaces off the serving path. Verify config LOADS, not just that it RENDERS. (Field report #356 #3.)
|
|
104
105
|
|
|
105
106
|
**L — Monitoring:** Health endpoint (/api/health checking DB, Redis, disk). External uptime monitor. Request logging (method, path, status, duration). Error tracking. Slow query logging (>1s). Worker job logging. Alerts: CPU >80%, Memory >85%, Disk >80%.
|
|
106
107
|
|
|
@@ -357,6 +358,10 @@ fi
|
|
|
357
358
|
```
|
|
358
359
|
When the path is root-owned and the agent is unprivileged, **emit the `sudo`-prefixed step as a MANUAL operator action** rather than attempting (and half-completing) the delete. A clean handoff beats a partial destruction. (`stat -c %U` is GNU coreutils; `stat -f %Su` is BSD/macOS — the snippet tries both for portability.)
|
|
359
360
|
|
|
361
|
+
### Pre-Build Disk Preflight (single-host Docker targets)
|
|
362
|
+
|
|
363
|
+
Before any `docker build` on a single-host target, check free space on the build filesystem — `npm ci` + `next build` + image export need headroom, and the export step fails at the very end with `no space left on device` after the whole build has run (~10 min wasted). SHA-tagged deploy images accumulate (e.g. 7 x ~1.5 GB) and silently consume the root volume. Preflight: `AVAIL=$(df -P /var/lib/docker | awk "NR==2{print \$4}")` (KB). If below a threshold (e.g. <8 GB, sized to image + build cache), do NOT just abort — offer remediation: (1) `docker builder prune -f` to clear build cache, (2) remove stale `app:<oldsha>` tags KEEPING the current rollback tag — `docker images --format "{{.Repository}}:{{.Tag}}" | grep "^app:" | grep -v -e ":latest" -e ":$ROLLBACK_SHA" | tail -n +3 | xargs -r docker rmi`. Re-check space, then build. Never auto-delete the rollback image. Evidence: field report #357 #1 — `docker build` failed at image export with host root at 96%; recovery freed ~11 GB.
|
|
364
|
+
|
|
360
365
|
## Multi-Environment Isolation
|
|
361
366
|
|
|
362
367
|
When staging and production coexist on the same server, enforce full isolation:
|
|
@@ -409,6 +414,8 @@ Add project-specific exclusions for any directory that receives runtime-generate
|
|
|
409
414
|
|
|
410
415
|
**Credential pre-flight:** Before any deploy, verify: (1) SSH_HOST is set, (2) SSH key file exists, (3) SSH test connection succeeds (`ssh -o ConnectTimeout=5`). If any check fails, abort — do not attempt deploy with missing credentials. Check `~/.voidforge/deploys/` and `~/.voidforge/projects.json` for historical credential data if `.env` is missing values.
|
|
411
416
|
|
|
417
|
+
**VCS config is a secret surface.** Treat `.git/config` like `.env`. An inline-token HTTPS remote (`https://user:TOKEN@github.com/...`) stores a live credential in plaintext and prints it on every `git remote -v` — into CI logs, deploy output, and screen-shares. Before deploy/CI runs, scan it: `grep -E 'https://[^/@]+:[^@]+@' .git/config`. Prefer SSH remotes (`git@github.com:owner/repo.git`) or a credential helper (`git config --global credential.helper`) over inline-token HTTPS — these keep the token out of `.git/config` and out of every remote op. If an inline token is found, rotate it and `git remote set-url` to SSH or a helper-backed URL. (Field report #361; companion: SECURITY_AUDITOR.md Phase-1 git-remote scan.)
|
|
418
|
+
|
|
412
419
|
**Type-check pre-flight:** Before any deploy, run `npx tsc --noEmit` (TypeScript) or equivalent type-checker. Deploy scripts must not proceed if type-checking fails. This catches errors that `npm run build` sometimes ignores (e.g., route params, config properties). Three consecutive deploy failures from catchable type errors is three too many. (Field report #299)
|
|
413
420
|
|
|
414
421
|
**Deploy target verification:** Before deploying to any platform (Vercel, Cloudflare, Netlify, etc.), verify the deploy target matches the intended production environment. If the project has multiple environments (preview, staging, production) or non-default production branches, use explicit flags (`--branch=main`, `--prod`). Never rely on default branch inference — it can silently deploy to the wrong environment. (Field report #114: 3 deploys to the wrong Vercel environment because the default branch was "main" but production was mapped to a different branch.)
|
|
@@ -417,6 +424,10 @@ Add project-specific exclusions for any directory that receives runtime-generate
|
|
|
417
424
|
|
|
418
425
|
**Email deliverability verification:** If the project sends email (transactional, auth, notifications), verify delivery works end-to-end after deploy: (1) Check that the sending domain has DNS records configured in the email provider (SPF, DKIM, domain verification). An API key alone is not enough — unverified domains silently fail with 403. (2) Send a test email via the provider's API (e.g., `curl` or SDK call) and confirm a 200 response. (3) If using a custom FROM domain, verify it matches the verified domain — mismatches cause silent rejection. Email that fails silently is invisible until a user reports "I never got the verification email." (Field report #259: Resend API key existed, templates existed, but sending domain was never verified in DNS — all emails silently 403'd for 2 weeks of production.)
|
|
419
426
|
|
|
427
|
+
**Live-fire verification per credential (field report #360).** After wiring ANY external credential — analytics, error tracking, ad platform, payment, LLM provider, anything with an API key/secret/token — exercise it against the provider's LIVE API and confirm acceptance before marking the integration done. Env-var-set is NOT done; a structurally-valid value (correct prefix/length) can still be dead. Send the smallest real authenticated request the provider supports (a no-op read, a token introspection, a `whoami`/`accounts:list`, a single test event) and assert a success status, not just a non-error transport. This single live call also surfaces latent integration bugs the stored value can't reveal: a hardcoded/sunsetting API version now returning 404 (pin a current version + add a health check), a missing required header (e.g. `login-customer-id` for a manager→client account), or wrong scopes. Evidence: a Google Ads credential that looked structurally valid was dead (`invalid_client`) and a v17 pin had been retired (404, current v21) — eyeballing would have shipped a silently-broken integration.
|
|
428
|
+
|
|
429
|
+
**Post-deploy OAuth sign-in failures: discriminate IdP-side from regression before rolling back.** When the first real sign-in after a deploy fails, do NOT reflexively roll back — first locate WHERE it failed. If the error page lives on the IdP's own domain (e.g. `accounts.google.com/info/unknownerror`, with a `rapt` re-auth token) and occurs BEFORE your `/callback` is hit, the failure is on the identity provider, not your migration — typically a stuck re-auth session, not a regression. Confirm your authorize request was well-formed (client_id, redirect_uri, scope, state) and then retry in a fresh/incognito session; an incognito success proves the deploy is fine and the IdP session was transient. Only an error AT your callback (state mismatch, token-exchange 4xx, cookie not set) implicates your code. A reflexive rollback on an IdP-side error falsely blames the migration and fixes nothing. (Field report #357 #3.)
|
|
430
|
+
|
|
420
431
|
**Post-deploy asset verification:** After deploying, verify specifically the files that *changed* in this deploy — not pre-existing assets. Check: (a) correct content-type header (text/html on a static asset means the file is missing from the deployment), (b) correct content-length (not the index.html fallback size), (c) deployment list shows the correct environment. Do NOT verify only pre-existing assets — they prove the host is up, not that the deploy succeeded. (Field report #114)
|
|
421
432
|
|
|
422
433
|
**Read back after a vendor PUT that doesn't echo the object.** When a deploy or config step `PUT`s to a vendor/control-plane API (DNS provider, CDN, Plex, a SaaS settings endpoint) and the response does **not** contain the mutated object, do NOT treat the `200` as confirmation — issue a follow-up `GET` and assert the field you set actually took (field report #353 RC-004). A vendor `PUT` can return `200 OK` while silently discarding body params it doesn't recognize, applies asynchronously, or rejects at a validation layer that still returns success (the Plex pattern: settings PUT returns 200 but the value is unchanged). The status code confirms the request was *received*, not that the *mutation persisted*. Rule: for any non-echoing PUT/PATCH on the deploy path, follow with a read-back and compare before declaring success.
|
|
@@ -505,11 +516,12 @@ Note: ahead-of-time-compiled binaries (Go, Rust, statically compiled C/C++) have
|
|
|
505
516
|
|
|
506
517
|
## Config Foot-Guns (deploy/runtime)
|
|
507
518
|
|
|
508
|
-
|
|
519
|
+
Four recurring config traps that pass every syntax check yet break at runtime (field report #352 #5):
|
|
509
520
|
|
|
510
521
|
- **Empty-string env defaults are non-nullish.** A shell default of the form `${VAR:-}` (or a Compose `VAR: ""`) sets the variable to `""`, which is a *defined, non-null* value. Downstream `cfg.X = process.env.VAR ?? defaultX` then keeps `""` — nullish coalescing (`??`) only fires on `null`/`undefined`, never on empty string — so the intended default is silently poisoned and the app runs with an empty config value. Either leave the var truly unset (omit the `:-` default) or validate-and-coerce empty strings at the config boundary.
|
|
511
522
|
- **Dev hostnames hardcoded in worker healthchecks false-fail in prod.** A worker healthcheck that pings `http://localhost:3000` or `redis://dev-redis` passes in dev and fails in prod, marking a healthy worker unhealthy (and triggering restart loops). Healthcheck targets must come from the same env config the worker uses, never literals.
|
|
512
523
|
- **Awaiting best-effort side effects on the auth path blocks sign-in.** `await analytics.track(...)` / `await auditLog.write(...)` inline in the login handler means a slow or down telemetry backend stalls — or fails — the sign-in. Best-effort side effects must be fire-and-forget (queue them, `void`-them, or move them off the request path), never `await`ed on a latency-critical auth route.
|
|
524
|
+
- **A strict-validated OPTIONAL env crashes at boot on the empty string compose forwards.** This is the empty-string trap one level deeper than the `??` case above. A new *optional* var forwarded through compose as `${VAR:-}` (or `VAR: ""`) reaches the app as `""`, not absent. A schema of the form `z.string().url().optional()` does NOT save you: `.optional()` only admits `undefined`, so `""` is treated as a present value and handed to `.url()` (or `.email()`, or an enum), which rejects it — config throws at module load and the worker crash-loops (health 500, prod outage). Forwarding the var in `x-prod-env` is necessary but not sufficient; the validator must tolerate what `${VAR:-}` actually produces. Normalize `'' -> undefined` BEFORE the strict check: `z.preprocess(v => (v === "" ? undefined : v), z.string().url().optional())`. Apply to every optional var carrying a strict format (URL, email, enum, regex). (Field report #356 #1.)
|
|
513
525
|
|
|
514
526
|
## Subdomain Routing (Cloudflare Pages / Vercel / Netlify)
|
|
515
527
|
|
|
@@ -104,6 +104,8 @@ This catches what static analysis misses: IPv6 binding, native module ABI compat
|
|
|
104
104
|
|
|
105
105
|
Why default-to-refuted: across instrumented Gauntlets, **~38% of first-pass Criticals were false positives** — author confidence and adversarial-attack momentum inflate severity. An attacker prompted to find bugs will manufacture them; a skeptic prompted to refute them filters them. The two passes are complementary: Crossfire (attack for new bugs) → Adversarial Verification (refute existing findings).
|
|
106
106
|
|
|
107
|
+
**Reproduce through the real execution path, not a model of it (field report #356 #2).** A CONFIRM vote backed by "I empirically reproduced it" counts only when the reproduction ran the value/input through the SAME execution path the code actually uses — the real CLI wrapper, the real tool invocation, the real runtime — not the underlying library exercised in isolation. A library called directly in a REPL can behave differently than the same library invoked through the CLI/tool/flag wrapper the script actually uses (different defaults, arg parsing, env, quoting, or output handling). "I reproduced the LIBRARY behavior" is NOT "the SCRIPT fails." Before a skeptic upgrades a finding to CONFIRM on empirical grounds, it must reproduce against the actual invocation (run the real command/script, not a hand-built isolation harness). In a #356 24-agent backup-script review, 2 of 16 confirmed findings were false positives precisely because the verifier reproduced a library in isolation; testing the real CLI invocation disproved both. When the real path cannot be exercised, downgrade to a code-read CONFIRM (subject to the same severity discount as any unreproduced finding) — never present an isolation reproduction as proof the real tool fails.
|
|
108
|
+
|
|
107
109
|
**Verify the FIX, not just the finding (field report #348 #4 / #350 #4):** The refute pass must also challenge the **PROPOSED FIX**, not only the finding it addresses. For each fix the batch intends to apply, the skeptic asks: *does this fix introduce a NEW failure mode the original code did not have?* Specifically hunt for **wedge, unbounded retry, infinite loop, orphaned record, double-send** regressions. The risk is acute whenever a fix adds a **coordination primitive — a sentinel, a lock, a retry-state row, a fence/claim marker — without also adding a liveness signal** (a bounded timeout that is actually reachable, a heartbeat, a dead-man release). A coordination primitive with no reachable release path does not fix a bug; it converts a transient failure into a permanent wedge.
|
|
108
110
|
|
|
109
111
|
> **M5 mint-fence incident (field report #348 #4):** a fix added a stale-reclaim fence to recover stuck mint jobs after **120s**. But the reclaim window sat *inside* a BullMQ retry budget of only **~3s** — the 120s liveness threshold was structurally unreachable before the job exhausted its retries, so drafts that hit the fence wedged permanently in `FAILED` instead of being reclaimed. The fix's own coordination primitive (the fence) had no reachable liveness signal. The finding was real; the *fix* created a new Critical.
|
|
@@ -121,6 +123,10 @@ Why default-to-refuted: across instrumented Gauntlets, **~38% of first-pass Crit
|
|
|
121
123
|
|
|
122
124
|
Troi also performs a **Marketing Copy Drift Check**: compare marketing page claims (features listed, capabilities described, performance promises) against the actual shipped feature set. Flag any claim that cannot be demonstrated in the running application. Marketing pages may describe planned features that were later descoped or changed during review fixes.
|
|
123
125
|
|
|
126
|
+
**Composition/wiring lens (Victory / multi-mission Gauntlet) (field report #358 #1):** Per-mission reviews are structurally blind to cross-mission composition — they only see one mission's changeset. A defect that is a property of the *assembled entry paths across all missions* (which code path is actually invoked at each armed/public entry point, what each entry *passes* vs. what the library *accepts*, and whether a security-critical default — `run_as`, eval tier, isolation flag — is set on the entry path or only deep in a module) is invisible to every per-mission review yet ships. Therefore the final holistic Gauntlet MUST dedicate at least one agent to a wiring/composition pass that: (1) enumerates every entry point (CLI, daemon, public route, scheduled job) that invokes the assembled system; (2) for each, traces what arguments/config it actually passes and reconciles them against what the library/eval gate accepts and what the safe default requires; (3) flags any entry path that omits a containment boundary the library threads internally but the entry never sets, or that injects a weaker gate (T1-only) than the full regression+isolation gate the system defines. This pass is non-negotiable and is not satisfied by green per-mission reviews. Field report #358: 12 passing reviews (10 per-mission + 2 every-4 checkpoints) all missed (a) every armed run executing as the privileged `run_as` user because it was set on the eval module but never on any entry path, and (b) both armed entry points injecting a T1-only eval that bypassed the T2/T3 gate — both caught only by the final Victory Gauntlet.
|
|
127
|
+
|
|
128
|
+
**Conditional verdict — ship-vs-enable separation (field report #358 #4):** When the Council's verdict is conditional — "safe to ship in state X but not state Y" (most commonly: safe to ship the feature GATED OFF, but NOT safe to arm/enable it) — the Council MUST NOT sign off on a bare "ship." It requires, before sign-off: (1) an **ADR that explicitly separates the two states** — what is true in the shipped-but-gated state, what must additionally hold before the enabled/armed state is safe (the open P0/P1 prerequisites), and which Gauntlet findings gate the transition; and (2) a **prerequisites runbook** enumerating the concrete, verifiable steps to move from shipped to enabled (containment boundary set on every entry path, full eval gate wired, credentials provisioned, etc.). Without this artifact, "shipped" silently reads as "fully enabled" to the next operator and a latent privileged-execution or gate-bypass gap goes live. The shipped state is only signed off once the ADR + runbook exist; the enabled state is signed off only once the runbook's prerequisites are independently verified. (Union Station's campaign wrote ADR-222 to capture exactly this separation.)
|
|
129
|
+
|
|
124
130
|
**Pattern auth completeness check (Kenobi, during Rounds 2-3):** When a pattern file defines an authentication flow, verify the auth checks perform actual value verification (compare against expected, call verify functions) — not just presence checks (`!!header`, `Boolean()`). Flag `!!` or truthiness checks on auth-related headers as suspicious. (Field report #109: daemon socket auth used `!!vaultHeader` which passed for any non-empty string.)
|
|
125
131
|
|
|
126
132
|
**Contrast-finding admissibility (Reality stone / a11y, Galadriel's team) (field report #355 F1):** A contrast finding is **inadmissible** — and therefore CANNOT be rated **Critical** or **High** — unless it cites the **literal source hex for BOTH the foreground and the background**, each with its own `file:line`, AND the agent re-greps that the offending **class pairing actually exists** at the cited location before rating. Citing a token *name* (`--text-muted on --surface-2`) is not a hex; the agent must resolve the token to its computed `#rrggbb` value at the cited `file:line` and quote both colors. An uncited contrast finding (no source hex, or only one of the two colors, or a class pairing that no longer exists at the cited line) is logged as inadmissible and dropped before the fix batch. This defends against the **token-name-swap false-Critical** — a finding that asserts a contrast failure from token names alone, where the swapped/renamed token actually resolves to a compliant hex and the failing class pairing was never present in the rendered output.
|
|
@@ -261,6 +261,10 @@ Oracle scans for methods that return success without side effects — the most d
|
|
|
261
261
|
|
|
262
262
|
Flag as **High severity**. In financial systems (trading, payments, billing), flag as **Critical**. (Field report #125: `ProtectionService._place_stop_loss()` returned `True` after logging but never called the exchange. `OrderService.cancel_order()` returned `True` without cancelling.)
|
|
263
263
|
|
|
264
|
+
### Real-Output Self-Test for LLM / External-Output Systems (field report #358 #2)
|
|
265
|
+
|
|
266
|
+
For any feature where the system consumes the output of an LLM or an external tool and then ACTS on it (applies an LLM-generated diff/edit, parses a model-authored JSON plan, executes a tool-returned command, validates a third-party payload), hand-authored fixtures are insufficient — they exercise only the shapes you imagined, which are exactly the shapes that already work. Mandate a **real-output self-test on seeded mutants**: seed a known defect (a real mutant), run the system end-to-end against the REAL external output (real LLM call, real tool response), and assert two properties — **does-it-fix** (the system resolves the seeded mutant) and **does-no-harm** (it does not corrupt unrelated state or pass when it should fail). **Heuristic: if every test of an integration boundary uses a fixture you authored, you have not tested the boundary — you have tested your own imagination of it.** Field report #358: M5–M9 unit tests fed the apply path hand-authored unified diffs that always `git apply`-ed cleanly; the first real-LLM self-test immediately surfaced that real Sonnet diffs do NOT apply (miscounted `@@` hunk headers, missing trailing newline → 'corrupt patch'). The fix was architectural (return exact `{old,new}` edits, generate the diff with `difflib`). Without a real-output self-test, this ships broken. Budget for flakiness: real-LLM tests hit rate limits — wrap each call in a bounded retry loop.
|
|
267
|
+
|
|
264
268
|
### Failure Attribution (multi-file test runs)
|
|
265
269
|
|
|
266
270
|
A test failure observed during a multi-file suite run is **NOT attributed to your change** until BOTH of these hold:
|
|
@@ -345,6 +349,8 @@ After running E2E tests, if the project has a running server, Batman launches th
|
|
|
345
349
|
|
|
346
350
|
0. **MANDATORY: Screenshot every page.** Before any forensic work, navigate to every primary route and take a screenshot. The agent MUST read each screenshot via the Read tool and inspect for: blank pages, error states, broken layouts, missing content. This is the "proof of life" gate — if a page is visibly broken, it's a finding before any deeper analysis begins.
|
|
347
351
|
|
|
352
|
+
0a. **Screenshotting a surface gated behind a down worker pipeline (field report #359):** When a review/confirmation surface is normally produced by an async worker (extraction job, render queue) and that pipeline is down — so you cannot reach the surface through the happy path to satisfy the mandatory screenshot gate — do NOT skip the screenshot. SEED the surface directly: insert a draft row into the DB (or call the seed/fixture endpoint) and load it via the app's existing deep-link (`?draft=<id>` or equivalent). The render path runs with no worker. This lets the proof-of-life gate complete and produces a real screenshot of the surface the operator will see, even when the upstream pipeline is unavailable.
|
|
353
|
+
|
|
348
354
|
1. **Console error sweep:** Navigate to every primary route. Capture all `pageerror` and `console.error` events (filtered per `browser-review.ts` pattern). Each uncaught exception is an automatic **High** finding with the error message, stack trace, and URL.
|
|
349
355
|
|
|
350
356
|
2. **Error state gallery:** For each primary API endpoint, use `page.route()` to force a 500 response. Screenshot the page. Verify: (a) user sees a meaningful error message, (b) page remains navigable, (c) no leaked internals (stack traces, SQL queries, file paths) in the error display.
|
|
@@ -78,6 +78,8 @@ These are independent, read-only scans. Run in parallel using the Agent tool:
|
|
|
78
78
|
|
|
79
79
|
**No credentials in git-tracked docs:** Never copy credentials from server-local files into git-tracked documentation. Reference the file location instead: 'Credentials are stored at /etc/app/.htpasswd' — not the actual password hash.
|
|
80
80
|
|
|
81
|
+
**Git remote / VCS credential scan:** Embedding a token in an HTTPS remote (`https://user:TOKEN@github.com/...`) is plaintext in `.git/config` and prints on every `git remote -v` (into logs, CI output, screen-shares, pasted bug reports) — a surface outside the code/env scope above. Scan it: run `git remote -v` and `grep -E 'https://[^/@]+:[^@]+@' .git/config` (also catch `x-access-token:` and `oauth2:` variants). Flag any match as CRITICAL — a live credential is exposed. Remediation: rotate the token immediately, then strip it from the remote — `git remote set-url origin git@github.com:<owner>/<repo>.git` (SSH) or switch to a credential helper (`git config --global credential.helper`), never an inline-token HTTPS URL. (Field report #361: a downstream session printed a live GitHub PAT on the very first `git remote -v` — the token sat in plaintext in `.git/config` and no existing check surfaced it.)
|
|
82
|
+
|
|
81
83
|
### Crypto Randomness
|
|
82
84
|
|
|
83
85
|
Verify all random value generation uses `crypto.getRandomValues()` (browser) or `crypto.randomBytes()` (Node.js). Flag `Math.random()` in any code that generates tokens, codes, identifiers, or secrets. `Math.random()` is predictable — an attacker can reconstruct the seed and predict future values. This is the most common security mistake in JavaScript codebases. (Field report #32: referral codes used Math.random() — caught by Gauntlet, not by build.)
|
|
@@ -263,6 +265,15 @@ For any system that sends URLs to users (transactional emails, SMS, push notific
|
|
|
263
265
|
|
|
264
266
|
This is the outbound mirror of SSRF prevention: SSRF stops external URLs from reaching internal services, outbound URL safety stops internal URLs from reaching external users. (Field report #44: verification email sent with `localhost:5005` URL — worked on same machine, broke from any other device.)
|
|
265
267
|
|
|
268
|
+
### Mandatory Adversarial Review: Untrusted-Data -> User-Facing-Sink (field report #359)
|
|
269
|
+
|
|
270
|
+
The adversarial security review is NOT author-discretionary for a change that introduces a NEW path from untrusted data to a user-facing sink. It is REQUIRED before deploy whenever a change adds any of:
|
|
271
|
+
- An extracted, user-supplied, or third-party URL embedded in a calendar event body, email, SMS, push, chat receipt (Telegram/Slack/Discord), webhook payload, or any rendered link a recipient can click.
|
|
272
|
+
- Untrusted text (model-extracted fields, scraped/OCR'd content, user free-text) flowing into one of those sinks.
|
|
273
|
+
- A new field copied verbatim from an untrusted source (e.g. a screenshot, an inbound webhook, an LLM extraction) that bypasses an existing security invariant (https-only link validation, allowlist, sanitizer).
|
|
274
|
+
|
|
275
|
+
Why mandatory: the change category most likely to carry a security regression is precisely the one authors are tempted to ship on 'it's low-risk.' Field report #359: a new untrusted `conference_url` (copied from a screenshot) bypassed the codebase's https-only `safeHttpsLink` invariant and would have reached the Calendar event body + Telegram/Slack/email receipts as a clickable open-redirect 'Join' link — caught only because the author chose to run the review. Make the choice mechanical, not discretionary. Maul + Windu run the open-redirect / link-injection / sink-egress checks (see Outbound URL Safety, Proxy Route SSRF, Response Header Injection) against the new path before the deploy gate clears.
|
|
276
|
+
|
|
266
277
|
### Enforcement-Layer Severity Rubric (field report #354 F2)
|
|
267
278
|
|
|
268
279
|
Key a finding's severity to the **enforcement layer**, not the **symptom location**. The question that sets severity is not "where did I see the leak?" but **"where is this actually enforced?"** Before you assign P0/P1, trace the request to the layer that *decides* — the server-side authorization check, the database query scope, the policy engine — and confirm the gap exists *there*.
|
|
@@ -181,6 +181,10 @@ This saves ~100K tokens on work that's far from execution. The full bridge crew
|
|
|
181
181
|
- Flag any dependency not updated in >12 months
|
|
182
182
|
- If project hasn't been touched in >30 days, this check is mandatory before any build work
|
|
183
183
|
|
|
184
|
+
### Dependency-Feasibility-First (framework/major-version migrations)
|
|
185
|
+
|
|
186
|
+
Before branching for a deferred-major or framework migration (e.g. Next 14→16, React 18→19, a major ORM/auth bump), confirm an ECOSYSTEM-COMPATIBLE version of every framework-coupled dependency exists FIRST — before any code churn. Query peer-dependency metadata deterministically: `npm view <pkg>@<version-or-range> peerDependencies` and confirm the target framework version satisfies the peer range. If NO published version of a required peer (auth adapter, router plugin, ORM driver) supports the target framework, STOP and mark the migration UPSTREAM-BLOCKED — do not branch, do not codemod, do not partially migrate against a peer that cannot resolve. Evidence: field report #357 — `npm view next-auth@<v> peerDependencies` showed beta.30 was the first to add `^16.0.0`; this answered feasibility before any branch was cut.
|
|
187
|
+
|
|
184
188
|
**Archer (Greenfield):** For new projects — proposes the initial directory structure, module boundaries, naming conventions, and bootstrap sequence. "Where no one has gone before."
|
|
185
189
|
**Kim (API Design):** REST conventions, consistent error shapes, pagination patterns, versioning strategy, GraphQL schema design. API surface architect.
|
|
186
190
|
**Pike (Bold Planning):** In `/campaign` — challenges Dax's mission ordering. "Should we attempt a harder mission first while context is fresh?" Bold decisions about sequencing.
|
|
@@ -274,6 +274,8 @@ See `/docs/patterns/e2e-test.ts` for the complete reference implementation:
|
|
|
274
274
|
|
|
275
275
|
**Mock signature verification:** When mocking external dependencies, verify the mocked methods exist on the real class. A mock that defines `sendMessage()` when the real SDK uses `send_message()` creates false confidence — tests pass but the integration fails. Pattern: `expect(Object.keys(mock)).toEqual(expect.arrayContaining(Object.keys(realInstance)))`.
|
|
276
276
|
|
|
277
|
+
**Author-fixture-only boundaries (LLM / external output):** If every test of an integration boundary feeds it a fixture you authored, you have not tested the boundary. Hand-authored inputs exercise only the shapes you imagined — and those already work. For any path that consumes LLM or external-tool output and acts on it (applies a model-generated diff, parses a model JSON plan, executes a tool-returned command), add at least one **real-output self-test on a seeded mutant** asserting does-it-fix and does-no-harm. (Field report #358: hand-authored diffs always git-applied; real Sonnet diffs did not — corrupt-patch bug invisible to every fixture test.) This complements, not contradicts, the existing "mock it, don't call it" rule below: that rule governs cheap deterministic dependencies; the seeded-mutant self-test governs the act-on-output integration boundary specifically.
|
|
278
|
+
|
|
277
279
|
**No source-code string assertions:** Never assert on status code strings or error class names found in source code (`'403' in source`, `'HTTPException' in source`). These break on any refactor that changes error handling mechanics (e.g., `HTTPException(403)` → `Errors.forbidden()`). Test the actual HTTP response status and body instead. (Field report #227)
|
|
278
280
|
|
|
279
281
|
**Error format migration checklist:** Before committing any change to error response shape (e.g., `{"detail": ...}` → `{"error": {"code", "message"}}`), grep test files for the old shape. Tests asserting `response["detail"]` will silently pass if the test never reaches the assertion (wrong status code) or will fail confusingly. Fix all test assertions to match the new shape in the same commit. (Field report #227)
|
|
@@ -37,6 +37,7 @@ Reference implementations for common code structures. These show the **shape and
|
|
|
37
37
|
| Design Tokens | `design-tokens.ts` | Semantic color/type tokens so theme pivots are a token change (field report #351) | CSS vars + Tailwind + React |
|
|
38
38
|
| Nginx Vhost | `nginx-vhost.conf` | Cloudflare-Flexible-safe vhost: security headers, ACME passthrough (field report #351) | Nginx |
|
|
39
39
|
| Error Message Categorization | `error-message-categorization.tsx` | Categorize errors at the UI boundary before showing copy (field report #351) | React (framework-agnostic notes) |
|
|
40
|
+
| Codemod Hygiene | `codemod-hygiene.md` | Strip incidental reformatting after a jscodeshift/recast codemod so the diff shows only the semantic change (field report #357) | jscodeshift/recast/`@next/codemod` |
|
|
40
41
|
|
|
41
42
|
## How to Use
|
|
42
43
|
|
|
@@ -234,9 +234,64 @@ const threadplexAgentStack: SafetyStack = {
|
|
|
234
234
|
* make it visible.
|
|
235
235
|
*/
|
|
236
236
|
|
|
237
|
+
// --- Lenient-schema + sanitize-at-trusted-boundary (untrusted extraction fields) ---
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Pattern for fields an LLM EXTRACTS from untrusted input (a scraped URL, an
|
|
241
|
+
* OCR'd 'join' link, a free-text field) that later flow to a security-sensitive
|
|
242
|
+
* sink (a calendar event body, an email, a chat receipt). Three forces collide:
|
|
243
|
+
* 1. Security — the field must satisfy an invariant before it reaches the sink
|
|
244
|
+
* (e.g. https-only; no open-redirect; allowlisted host).
|
|
245
|
+
* 2. Extraction robustness — one bad optional field must NOT hard-fail the
|
|
246
|
+
* whole extraction; the rest of the structured output is still good.
|
|
247
|
+
* 3. Edit-data-loss — silently dropping the field at the schema loses data the
|
|
248
|
+
* operator could have corrected on the review surface.
|
|
249
|
+
*
|
|
250
|
+
* Resolution (field report #359): do NOT enforce the security invariant at the
|
|
251
|
+
* extraction schema. Validate the field LENIENTLY at the schema (accept the raw
|
|
252
|
+
* string, never hard-fail the extraction), normalize it in the ADAPTER, and
|
|
253
|
+
* enforce the invariant at the TRUSTED CONSUMER BOUNDARY — the code that writes
|
|
254
|
+
* into the sink. The sink-writer is the single choke point that decides whether
|
|
255
|
+
* the link is clickable; that is where https-only lives.
|
|
256
|
+
*/
|
|
257
|
+
export interface UntrustedExtractionField {
|
|
258
|
+
name: string
|
|
259
|
+
schemaPolicy: 'lenient' // accept raw; never hard-fail the whole extraction
|
|
260
|
+
normalizeIn: string // adapter location that trims/normalizes the raw value
|
|
261
|
+
invariant: string // e.g. 'https-only, no open-redirect, allowlisted host'
|
|
262
|
+
enforcedAt: string // the TRUSTED consumer boundary that writes the sink
|
|
263
|
+
onInvariantFail: 'omit-from-sink-keep-for-edit' // drop from the clickable sink, retain on the review surface
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const conferenceUrlField: UntrustedExtractionField = {
|
|
267
|
+
name: 'conference_url',
|
|
268
|
+
schemaPolicy: 'lenient',
|
|
269
|
+
normalizeIn: 'calendar adapter — trim, lowercase scheme',
|
|
270
|
+
invariant: 'https-only; no open-redirect; matches known conferencing-host allowlist',
|
|
271
|
+
enforcedAt: 'safeHttpsLink() at the event-body / receipt writer (the sink choke point)',
|
|
272
|
+
onInvariantFail: 'omit-from-sink-keep-for-edit',
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ANTI-PATTERN 4: enforce the security invariant at the extraction schema
|
|
276
|
+
*
|
|
277
|
+
* 'We made conference_url a z.string().url().startsWith("https://") in the
|
|
278
|
+
* extraction schema, so a bad link can never reach the sink.'
|
|
279
|
+
*
|
|
280
|
+
* No. A hard schema constraint on ONE optional extracted field fails the WHOLE
|
|
281
|
+
* extraction when the model returns a non-https or malformed value, discarding
|
|
282
|
+
* the good fields too (force #2), and silently losing data the operator could
|
|
283
|
+
* have fixed (force #3). Worse, a field copied verbatim from an untrusted
|
|
284
|
+
* source that BYPASSES the schema path (added later, normalized elsewhere)
|
|
285
|
+
* reaches the sink unchecked — the exact open-redirect of field report #359.
|
|
286
|
+
*
|
|
287
|
+
* Fix: lenient schema, enforce https-only at the sink writer (safeHttpsLink),
|
|
288
|
+
* surface the raw value on the review surface for operator edit.
|
|
289
|
+
*/
|
|
290
|
+
|
|
237
291
|
export {
|
|
238
292
|
authorityInstruction,
|
|
239
293
|
denyListEnforcement,
|
|
240
294
|
fsPermsEnforcement,
|
|
241
295
|
threadplexAgentStack,
|
|
296
|
+
conferenceUrlField,
|
|
242
297
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Pattern: Codemod Hygiene (strip incidental reformatting)
|
|
2
|
+
|
|
3
|
+
**When to use:** Any AST codemod run (jscodeshift, `@next/codemod`, `react-codemod`, or a hand-rolled recast transform) over a codebase with pre-existing format debt.
|
|
4
|
+
|
|
5
|
+
**Source:** Field report #357 §4.
|
|
6
|
+
|
|
7
|
+
## The Failure Mode
|
|
8
|
+
|
|
9
|
+
AST codemods built on recast (jscodeshift, `@next/codemod`, `react-codemod`) preserve formatting for nodes they DON'T touch but RE-PRINT touched nodes from the AST — so any file with pre-existing format debt (irregular JSX wrapping, multi-line object style, mixed quotes) gets reformatted beyond the semantic change, inflating the diff and burying the real change.
|
|
10
|
+
|
|
11
|
+
## Hygiene Procedure
|
|
12
|
+
|
|
13
|
+
1. Run the codemod on a clean tree.
|
|
14
|
+
2. Review the diff and separate semantic hunks from reformatting hunks.
|
|
15
|
+
3. For files where reformatting dominates, `git checkout -p` / revert the incidental hunks and re-apply ONLY the semantic change by hand.
|
|
16
|
+
4. OR run the project formatter (prettier/eslint --fix) scoped to changed files BEFORE the codemod so the codemod's reprint matches existing style, making the diff semantic-only.
|
|
17
|
+
|
|
18
|
+
## The Trade-off
|
|
19
|
+
|
|
20
|
+
Option (4) is cleaner for well-formatted codebases; option (3) is right when format debt is intentional/unowned. (Field report #357 §4.)
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
* - Deploy-strategy claims must be backed by a real mechanism: a comment that
|
|
16
16
|
* says "blue-green"/"zero-downtime" without an atomic swap (rename, container
|
|
17
17
|
* swap, or LB cutover) is a lie that ships a 502 window (#343 F7).
|
|
18
|
+
* - #361 git-remote credential check: an opt-in scan of .git/config for inline
|
|
19
|
+
* credentials baked into a remote URL (https://user:token@host/...). .git/ is
|
|
20
|
+
* OUTSIDE the deploy artifact, so the artifact walk never reaches it; this scan
|
|
21
|
+
* runs independently against the repo root (process.cwd() or --git-root). It is
|
|
22
|
+
* best-effort — a checkout with no local .git is fine — and only ever reports
|
|
23
|
+
* the path + pattern id, never the matched credential.
|
|
18
24
|
*
|
|
19
25
|
* Usage:
|
|
20
26
|
* npx tsx docs/patterns/deploy-preflight.ts ./dist
|
|
@@ -26,7 +32,7 @@
|
|
|
26
32
|
|
|
27
33
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
28
34
|
import { extname, join, relative, sep } from 'node:path';
|
|
29
|
-
import { argv, env, exit } from 'node:process';
|
|
35
|
+
import { argv, cwd, env, exit } from 'node:process';
|
|
30
36
|
|
|
31
37
|
// ---------- forbidden filename patterns ----------
|
|
32
38
|
const FORBIDDEN_NAME_PATTERNS: { id: string; test: (name: string, rel: string) => boolean }[] = [
|
|
@@ -50,6 +56,9 @@ const FORBIDDEN_CONTENT_PATTERNS: { id: string; re: RegExp }[] = [
|
|
|
50
56
|
{ id: 'cloudflare-token', re: /\b[0-9a-f]{40}\b/ },
|
|
51
57
|
{ id: 'github-pat', re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
|
|
52
58
|
{ id: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
|
59
|
+
// #361: inline credentials baked into a remote URL — https://user:token@host/...
|
|
60
|
+
// (also covers x-access-token:/oauth2: user fields). Used by the .git/config scan.
|
|
61
|
+
{ id: 'git-remote-inline-credential', re: /https:\/\/[^/@\s]+:[^@\s]+@/ },
|
|
53
62
|
];
|
|
54
63
|
|
|
55
64
|
const TEXT_EXTENSIONS = new Set([
|
|
@@ -59,7 +68,7 @@ const TEXT_EXTENSIONS = new Set([
|
|
|
59
68
|
]);
|
|
60
69
|
|
|
61
70
|
interface Hit {
|
|
62
|
-
kind: 'name' | 'content' | 'strategy';
|
|
71
|
+
kind: 'name' | 'content' | 'strategy' | 'git-config';
|
|
63
72
|
path: string;
|
|
64
73
|
patternId: string;
|
|
65
74
|
}
|
|
@@ -205,6 +214,36 @@ function scanContent(fullPath: string): string | null {
|
|
|
205
214
|
return null;
|
|
206
215
|
}
|
|
207
216
|
|
|
217
|
+
// ---------- #361 git-remote inline-credential scan ----------
|
|
218
|
+
// .git/ is OUTSIDE the deploy artifact, so walk(target) never reaches it. This
|
|
219
|
+
// runs independently against the repo root and inspects .git/config for a remote
|
|
220
|
+
// URL with embedded credentials (https://user:token@host/...). Best-effort: a
|
|
221
|
+
// checkout without a local .git is a clean no-op. NEVER returns or logs the
|
|
222
|
+
// matched credential — only that a match occurred (path + pattern id upstream).
|
|
223
|
+
const GIT_REMOTE_CREDENTIAL_RE = /https:\/\/[^/@\s]+:[^@\s]+@/;
|
|
224
|
+
|
|
225
|
+
function scanGitConfig(gitRoot: string): boolean {
|
|
226
|
+
const configPath = join(gitRoot, '.git', 'config');
|
|
227
|
+
let stats;
|
|
228
|
+
try {
|
|
229
|
+
stats = statSync(configPath);
|
|
230
|
+
} catch {
|
|
231
|
+
return false; // no local .git/config — best-effort no-op
|
|
232
|
+
}
|
|
233
|
+
if (!stats.isFile()) return false;
|
|
234
|
+
if (stats.size > 2_000_000) return false;
|
|
235
|
+
let buf: string;
|
|
236
|
+
try {
|
|
237
|
+
buf = readFileSync(configPath, 'utf8');
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
for (const line of buf.split('\n')) {
|
|
242
|
+
if (GIT_REMOTE_CREDENTIAL_RE.test(line)) return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
208
247
|
function main(): void {
|
|
209
248
|
const target = argv[2];
|
|
210
249
|
if (!target) {
|
|
@@ -253,6 +292,18 @@ function main(): void {
|
|
|
253
292
|
}
|
|
254
293
|
}
|
|
255
294
|
|
|
295
|
+
// #361 git-remote inline-credential check. .git/ lives outside the deploy
|
|
296
|
+
// artifact, so resolve the repo root from a --git-root flag (or process.cwd())
|
|
297
|
+
// rather than from `target`. Best-effort: a checkout with no local .git is a
|
|
298
|
+
// clean no-op so CI runs that deploy from a bare artifact don't break.
|
|
299
|
+
const gitRootFlagIdx = argv.indexOf('--git-root');
|
|
300
|
+
const gitRoot =
|
|
301
|
+
gitRootFlagIdx !== -1 && argv[gitRootFlagIdx + 1] ? argv[gitRootFlagIdx + 1] : cwd();
|
|
302
|
+
if (scanGitConfig(gitRoot)) {
|
|
303
|
+
// NEVER print the matched credential — only the path + pattern id.
|
|
304
|
+
hits.push({ kind: 'git-config', path: '.git/config', patternId: 'git-remote-inline-credential' });
|
|
305
|
+
}
|
|
306
|
+
|
|
256
307
|
const summary = {
|
|
257
308
|
action: 'deploy-preflight',
|
|
258
309
|
target,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voidforge-build",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.13.1",
|
|
4
4
|
"description": "From nothing, everything. A methodology framework for building with Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@aws-sdk/client-rds": "^3.700.0",
|
|
46
46
|
"@aws-sdk/client-s3": "^3.700.0",
|
|
47
47
|
"@aws-sdk/client-sts": "^3.700.0",
|
|
48
|
-
"voidforge-build-methodology": "^23.
|
|
48
|
+
"voidforge-build-methodology": "^23.13.1",
|
|
49
49
|
"node-pty": "^1.2.0-beta.12",
|
|
50
50
|
"ws": "^8.19.0"
|
|
51
51
|
},
|