@nurix/etna 0.15.3 → 0.15.5
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/bin/etna.js +22 -7
- package/data/harness-index.json +1 -0
- package/data/harnesses/code/CLAUDE.md +3 -3
- package/data/harnesses/code/agents/harness-manager.md +2 -2
- package/data/harnesses/code/agents/spec-mode.md +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/kb-agi/harness.json +1 -1
- package/data/harnesses/re/CLAUDE.md +9 -2
- package/data/harnesses/re/agents/infra-backend-fingerprint.md +9 -0
- package/data/harnesses/re/agents/session.md +8 -6
- package/data/harnesses/re/agents/website.md +1 -1
- package/data/harnesses/re/agents/wire-capture.md +7 -5
- package/data/harnesses/re/harness.json +2 -2
- package/data/harnesses/re/rules/re-evaluation.md +9 -3
- package/data/harnesses/re/rules/re-ingestion.md +57 -5
- package/package.json +1 -1
package/bin/etna.js
CHANGED
|
@@ -186,32 +186,45 @@ async function main(args) {
|
|
|
186
186
|
const targetDir = process.cwd();
|
|
187
187
|
const claudeMdDest = join(targetDir, 'CLAUDE.md');
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
// Read the bundled epoch marker up front so the install banner can name the
|
|
190
|
+
// harness version; the file itself is copied into the project further down.
|
|
191
|
+
const bundledHarnessJson = join(bundledHarnessDir, 'harness.json');
|
|
192
|
+
const harnessMeta = existsSync(bundledHarnessJson) ? readJson(bundledHarnessJson) : {};
|
|
193
|
+
const versionTag = harnessMeta.version ? ` v${harnessMeta.version}` : '';
|
|
194
|
+
|
|
195
|
+
console.log(`\nInstalling ${pluginName}${versionTag} into ${targetDir}`);
|
|
190
196
|
|
|
191
197
|
const claudeBackup = backupIfExists(claudeMdDest);
|
|
192
198
|
if (claudeBackup) console.log(` backed up existing CLAUDE.md → ${claudeBackup}`);
|
|
193
199
|
|
|
194
200
|
// CLAUDE.md ships frontmatter-free (the harness's metadata lives in its index.md and the
|
|
195
201
|
// plugin's definition.md), so it installs as a verbatim copy — no stripping.
|
|
202
|
+
// Tracks the categories actually present in this harness, for the closing
|
|
203
|
+
// summary — so a harness that ships no skills or hooks reads as an explicit,
|
|
204
|
+
// honest list rather than a silently missing line.
|
|
205
|
+
const installed = [];
|
|
206
|
+
|
|
196
207
|
cpSync(bundledClaudeMd, claudeMdDest, { force: true });
|
|
197
208
|
console.log(` installed CLAUDE.md → ${claudeMdDest}`);
|
|
209
|
+
installed.push('CLAUDE.md');
|
|
198
210
|
|
|
199
211
|
for (const sub of PLUGIN_SUBDIRS) {
|
|
200
212
|
const count = copyHarnessSubdir(
|
|
201
213
|
join(bundledHarnessDir, sub),
|
|
202
214
|
join(targetDir, '.claude', sub),
|
|
203
215
|
);
|
|
204
|
-
if (count > 0)
|
|
216
|
+
if (count > 0) {
|
|
217
|
+
console.log(` copied ${count} ${sub} → ${targetDir}/.claude/${sub}/`);
|
|
218
|
+
installed.push(`${count} ${sub}`);
|
|
219
|
+
}
|
|
205
220
|
}
|
|
206
221
|
|
|
207
222
|
// Copy the epoch marker so harness-manager can resolve the installed epoch.
|
|
208
|
-
const bundledHarnessJson = join(bundledHarnessDir, 'harness.json');
|
|
209
223
|
if (existsSync(bundledHarnessJson)) {
|
|
210
224
|
const harnessJsonDest = join(targetDir, '.claude', 'harness.json');
|
|
211
225
|
mkdirSync(dirname(harnessJsonDest), { recursive: true });
|
|
212
226
|
cpSync(bundledHarnessJson, harnessJsonDest, { force: true });
|
|
213
|
-
const
|
|
214
|
-
const tag = [meta.version && `v${meta.version}`, meta.epoch && `epoch ${meta.epoch}`]
|
|
227
|
+
const tag = [harnessMeta.version && `v${harnessMeta.version}`, harnessMeta.epoch && `epoch ${harnessMeta.epoch}`]
|
|
215
228
|
.filter(Boolean)
|
|
216
229
|
.join(', ');
|
|
217
230
|
console.log(` installed harness.json → ${harnessJsonDest}${tag ? ` (${tag})` : ''}`);
|
|
@@ -226,8 +239,10 @@ async function main(args) {
|
|
|
226
239
|
const migrated = migrateFromPluginInstall(targetDir, pluginName);
|
|
227
240
|
for (const entry of migrated) console.log(` removed stale ${entry}`);
|
|
228
241
|
|
|
229
|
-
console.log(`\nDone
|
|
230
|
-
console.log(
|
|
242
|
+
console.log(`\nDone — ${pluginName}${versionTag} installed: ${installed.join(', ')}.`);
|
|
243
|
+
console.log(
|
|
244
|
+
`Open Claude Code in ${targetDir}. Re-run \`npx @nurix/etna ${pluginName}\` anytime to refresh harness content.\n`,
|
|
245
|
+
);
|
|
231
246
|
}
|
|
232
247
|
|
|
233
248
|
main(process.argv.slice(2)).catch((e) => {
|
package/data/harness-index.json
CHANGED
|
@@ -20,7 +20,7 @@ This is the only step that runs **before** the harness exists; everything below
|
|
|
20
20
|
|
|
21
21
|
## Getting Started
|
|
22
22
|
|
|
23
|
-
- Read **[`docs/
|
|
23
|
+
- Read **[`docs/structure.md`](docs/structure.md)** for the documentation map and folder authority (architecture, features, operations)
|
|
24
24
|
- Read **[`docs/memory/`](docs/memory/)** for agent memory files (one per agent)
|
|
25
25
|
- Ignore **[`README.md`](README.md)** – it's for human reference only
|
|
26
26
|
- Check **[`package.json`](package.json)** for available scripts. Detect the package manager from the lockfile (`pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `package-lock.json` → npm) and use the matching command. Don't hardcode a specific manager.
|
|
@@ -134,9 +134,9 @@ Unless explicitly requested:
|
|
|
134
134
|
|
|
135
135
|
## Documentation
|
|
136
136
|
|
|
137
|
-
`docs/structure.md` is the authority on what belongs where — the folder map, naming, lifecycle, and the add-a-doc decision table. Read it before adding or moving a doc; don't restate it here.
|
|
137
|
+
`docs/structure.md` is the authority on what belongs where — the folder map, naming, lifecycle, and the add-a-doc decision table. Read it before adding or moving a doc; don't restate it here. One canonical source per topic — link, don't copy. Only `CLAUDE.md`, `AGENTS.md`, and `README.md` live at the repo root.
|
|
138
138
|
|
|
139
139
|
Maintenance triggers (act on these without re-reading `structure.md`):
|
|
140
140
|
|
|
141
141
|
- After plan completion: add a `docs/history/changelog/` entry + update `docs/history/CHANGELOG.md` (no confirmation; include `Commits:` if multi-commit). Non-plan changes (refactors, fixes, API changes): prompt first.
|
|
142
|
-
- When you add, move, or delete a doc: update `docs/routing.md`
|
|
142
|
+
- When you add, move, or delete a doc: update `docs/routing.md` to match. When you move or rename source files: fix the `owns`/`touches` frontmatter in the owning doc; new docs that own code need `owns`/`touches`/`depends_on`.
|
|
@@ -104,7 +104,7 @@ Table: `| File | Action | From | To | Reason |` (actions: `Move`, `Merge into {t
|
|
|
104
104
|
|
|
105
105
|
Create the folder structure; move/fold/merge/delete per the plan; add `Status:` to plan files; add ownership frontmatter to feature docs (call the `documentation` skill's **frontmatter derivation** for `owns`/`touches`/`depends_on`); merge scattered backlogs into `docs/history/backlog.md`; fix links broken by moves. Scaffold missing mandated files **by calling the `documentation` skill for each template** — you orchestrate, the skill supplies templates:
|
|
106
106
|
|
|
107
|
-
- `docs/
|
|
107
|
+
- `docs/structure.md` (with `harness-version`), `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/index.md`, `docs/memory/main.md`, `docs/memory/facts/.digest-state.md` (placeholder, "no entries digested yet"), and the repo-root `README.md` (filled from the real lockfile/scripts/layout — **never overwrite an existing root README**).
|
|
108
108
|
|
|
109
109
|
Scaffold `docs/memory/facts/` so the distilled tier has a home from day one: hooks write the raw ledger to `docs/memory/ledger/` (not a scaffold — created mechanically on first event), and the `distiller` digests it down into fact-cards under `docs/memory/facts/`. Without the facts dir + its `.digest-state.md` high-water mark, a freshly-baselined consumer accrues a ledger firehose the distiller has nowhere to land — so seed the empty distilled tier alongside the hook-written ledger tier.
|
|
110
110
|
|
|
@@ -160,7 +160,7 @@ If the user **declines** the advisory rather than running the follow-up, record
|
|
|
160
160
|
|
|
161
161
|
### Step 7 — Verify
|
|
162
162
|
|
|
163
|
-
Confirm all mandated files exist; `structure.md` carries the current `harness-version`; no dead links in `docs/
|
|
163
|
+
Confirm all mandated files exist; `structure.md` carries the current `harness-version`; no dead links in `docs/structure.md`; every plan has a `Status:`; feature docs have ownership frontmatter; no dissolved-version artifacts remain; no docs outside the canonical structure.
|
|
164
164
|
|
|
165
165
|
---
|
|
166
166
|
|
|
@@ -17,7 +17,7 @@ You do NOT write the spec — you return structured research that the main agent
|
|
|
17
17
|
|
|
18
18
|
First, understand the documentation landscape:
|
|
19
19
|
|
|
20
|
-
1. Read `./docs/
|
|
20
|
+
1. Read `./docs/structure.md` to understand the documentation structure
|
|
21
21
|
2. If `./docs/features/README.md` exists, read it to see existing feature specs and where the new spec fits; otherwise glob the feature docs directly
|
|
22
22
|
3. If `./docs/memory/spec-mode.md` exists, read it for feature doc inventory, ownership conflict zones, and lessons from previous research
|
|
23
23
|
4. Read 1-2 existing specs in `./docs/features/` to calibrate format and depth
|
|
@@ -69,11 +69,18 @@ method · confidence · completeness · gaps) + `raw/`, obeying `.claude/rules/r
|
|
|
69
69
|
|
|
70
70
|
- **Run independents in parallel** — dispatch `docs`, `packages`, `api`, `website`, `community`,
|
|
71
71
|
`deployed-client-bundle`, `infra-backend-fingerprint`, `distribution-artifacts` together
|
|
72
|
-
(`subagent_type='<dimension>'`, one message, multiple Agent calls).
|
|
72
|
+
(`subagent_type='<dimension>'`, one message, multiple Agent calls). Seed each dumper with **warm
|
|
73
|
+
entry points (URLs / repos / hosts), not claimed facts** — any fact carried from Discovery (a price,
|
|
74
|
+
a tier name, a version) is tagged `verify:may-be-stale` so the dumper re-derives it instead of
|
|
75
|
+
anchoring on a possibly-stale value. Only one dimension may drive the browser at a time (see
|
|
76
|
+
`re-ingestion` §7 rule 9): the parallel dumpers use WebFetch/curl, leaving Chrome to `session`.
|
|
73
77
|
- **Codebase fan-out** — dispatch `codebase`; it clones + maps + returns a per-package work-list;
|
|
74
78
|
then the main session fans out one reader per substantial package.
|
|
75
79
|
- **Gated dimensions** — `session` and `wire-capture` (rung ≥2) need explicit user authorization
|
|
76
|
-
first (see `re-ingestion` §7). `session` follows `.claude/rules/re-ingestion.md` §9.
|
|
80
|
+
first (see `re-ingestion` §7). `session` follows `.claude/rules/re-ingestion.md` §9. When `session`
|
|
81
|
+
is authorized, **dispatch the non-browser dumpers in the background (`run_in_background`) and drive
|
|
82
|
+
the gated browser session in the main session concurrently** — they don't contend (Chrome belongs to
|
|
83
|
+
the main session; the dumpers use WebFetch/curl), so the whole of Ingestion runs in parallel.
|
|
77
84
|
- **Shared seam** — bundle/session/wire each contribute to the canonical `raw/api-path-catalog.md`
|
|
78
85
|
with a `source:` column; don't triplicate.
|
|
79
86
|
|
|
@@ -95,3 +95,12 @@ any CT-leaked host worth a `wire-capture` or `deployed-client-bundle` follow-up.
|
|
|
95
95
|
crt.sh/WHOIS rate limits and back off; redact any credential-shaped value before writing (contract
|
|
96
96
|
§7 rule 3 — DSNs, verification tokens, embedded keys surfaced in headers/TXT). An unresolvable host or a
|
|
97
97
|
missing sub-processor page is an Open question, not a guess.
|
|
98
|
+
|
|
99
|
+
**Verify an absence before asserting it (contract §7 rule 10).** Before reporting any host as
|
|
100
|
+
**NXDOMAIN / absent** — especially `api.<target>` or any host a sibling dimension (bundle / session /
|
|
101
|
+
wire) reports actually hitting — re-resolve via a **second resolver** (`dig @1.1.1.1 <host>`,
|
|
102
|
+
`dig @8.8.8.8 <host>`) **and** a direct `curl -sI https://<host>`. An HTTP 4xx/5xx response means the
|
|
103
|
+
host **resolves** → report it **present**, not absent. A single negative resolver result is recorded as
|
|
104
|
+
*tentative*, never stated as fact and never used to instruct sibling dimensions. Likewise, an *inferred*
|
|
105
|
+
backend engine (e.g. "looks like Hasura/Supabase from the CNAME") is phrased as an inference and yields to
|
|
106
|
+
a runtime-observing dimension (`session`/`deployed-client-bundle`) that names the engine directly.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: session
|
|
3
3
|
description: Dimension dumper for the LOGGED-IN APP SESSION dimension of the Product Recon Harness — the authenticated runtime behind login. Delegates to the copied-in browser-session playbook (three-pass, P0–P6) to instrument the live product via Chrome MCP and capture its wire protocol, auth, and state model. The deepest and most sensitive dimension; opt-in, user-gated, read-only-by-default. Dispatched during Ingestion (Mode 2) only after the user has a session ready and authorizes it.
|
|
4
|
-
tools: Read, Write, Bash, Glob, Grep
|
|
4
|
+
tools: Read, Write, Bash, Glob, Grep, mcp__claude-in-chrome__tabs_context_mcp, mcp__claude-in-chrome__tabs_create_mcp, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__read_network_requests, mcp__claude-in-chrome__read_console_messages, mcp__claude-in-chrome__javascript_tool, mcp__claude-in-chrome__get_page_text
|
|
5
5
|
model: inherit
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -27,11 +27,13 @@ It is **interactive and gated**, unlike the other six:
|
|
|
27
27
|
the main session with the exact proposed action + estimated cost so the *user* authorizes it
|
|
28
28
|
in chat. Browser instrumentation that changes state must go through that confirmation loop.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
You now hold **read-only Chrome tools**, so you run **Pass 1 (read-only mining) directly** — verify
|
|
31
|
+
the §9.1 preconditions with `mcp__claude-in-chrome__tabs_context_mcp` (live tabs + stable IDs prove
|
|
32
|
+
the extension and CDP are connected), then mine the authenticated surface yourself with
|
|
33
|
+
`read_network_requests` + an in-page `javascript_tool` interceptor. **Pass 2 (any state-changing /
|
|
34
|
+
cost-incurring action) is NOT yours to trigger** — return to the **main session** with the proposed
|
|
35
|
+
action + estimated cost so the *user* authorizes it in chat (the confirmation loop above). The main
|
|
36
|
+
session still owns the user dialogue and any state-changing instrumentation.
|
|
35
37
|
|
|
36
38
|
## Output (the playbook's own contract — richer than the standard dump)
|
|
37
39
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: website
|
|
3
3
|
description: Dimension dumper for the MARKETING WEBSITE dimension of the Product Recon Harness — homepage, product/feature pages, pricing, customers, integrations, blog index. Extracts positioning, feature claims, pricing tiers/limits, target market, and the integration list. Dispatched during Ingestion (Mode 2).
|
|
4
|
-
tools: Read, Write, Bash, WebFetch, WebSearch
|
|
4
|
+
tools: Read, Write, Bash, WebFetch, WebSearch, mcp__claude-in-chrome__tabs_create_mcp, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__get_page_text
|
|
5
5
|
model: inherit
|
|
6
6
|
skills:
|
|
7
7
|
- fetch
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wire-capture
|
|
3
3
|
description: Dimension dumper for the GATED WIRE-CAPTURE dimension of the Product Recon Harness. Climbs the 3-rung lowest-privilege-first ladder (browser tap → HAR → user-run mitmproxy) to record what a non-browser/no-source client *actually* calls, redacting every body before disk. Dispatched during Ingestion (Mode 2) — not invoked directly by users.
|
|
4
|
-
tools: Read, Write, Bash
|
|
4
|
+
tools: Read, Write, Bash, mcp__claude-in-chrome__tabs_context_mcp, mcp__claude-in-chrome__tabs_create_mcp, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__read_network_requests, mcp__claude-in-chrome__read_console_messages, mcp__claude-in-chrome__javascript_tool, mcp__claude-in-chrome__get_page_text
|
|
5
5
|
model: inherit
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -13,10 +13,12 @@ first — especially **§7.1** (the 3-rung ladder), §6 (the shared api-path-cat
|
|
|
13
13
|
redaction rules. You obey it absolutely. This is the **runtime complement** to
|
|
14
14
|
`distribution-artifacts`: the binary says what hosts it *might* call; you record what it *does*.
|
|
15
15
|
|
|
16
|
-
This dimension is **GATED**.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
This dimension is **GATED at rungs 2–3**. You now hold read-only Chrome tools, so **rung 1 — the
|
|
17
|
+
public, unauth browser tap — you run directly**: open the public surface in a tab and tap it with
|
|
18
|
+
`mcp__claude-in-chrome__read_network_requests` + an in-page `mcp__claude-in-chrome__javascript_tool`
|
|
19
|
+
interceptor. **Rungs 2–3 stay interactive and user-run** — the user routes their own client and you
|
|
20
|
+
ingest the exported HAR / flows. Author the captures, and **flag higher rungs for user
|
|
21
|
+
authorization** rather than escalating on your own.
|
|
20
22
|
|
|
21
23
|
## Inputs (from the dispatch prompt)
|
|
22
24
|
- target slug + the output dir `research/<target>/dimensions/wire-capture/`,
|
|
@@ -11,7 +11,11 @@ coherent picture, **weighted by how each claim was obtained**, with disagreement
|
|
|
11
11
|
`_summary.md` frontmatter.
|
|
12
12
|
2. **Weight, don't average.** A claim's confidence = the **max** of its sources' confidence,
|
|
13
13
|
**promoted one band only if corroborated by ≥2 *independent* dimensions**, capped at source
|
|
14
|
-
confidence when single-source. Conflicts are **flagged, never averaged**.
|
|
14
|
+
confidence when single-source. Conflicts are **flagged, never averaged**. **Tiebreaker — direct
|
|
15
|
+
observation outranks inference.** When two sources conflict on the *same* fact, a directly-observed
|
|
16
|
+
value beats an inferred one regardless of dimension or stated confidence (a session-observed backend
|
|
17
|
+
engine beats a DNS-inferred one; a live HTTP probe beats a single NXDOMAIN result). The inferred
|
|
18
|
+
side is rendered tentative and never used to override the observation or to issue cross-dimension directives.
|
|
15
19
|
3. **Cross-check, don't concatenate.** Agree → state as fact. Disagree / single-source / low-conf →
|
|
16
20
|
render hedged ("observed but unverified…") and flag in the doc's reconciliation section.
|
|
17
21
|
|
|
@@ -52,8 +56,10 @@ prose, not via a separate `claims.yaml`.)
|
|
|
52
56
|
## 3. `evaluation/data-model-api-surface.md`
|
|
53
57
|
```markdown
|
|
54
58
|
# <Target> — Data Model & API / SDK Surface
|
|
55
|
-
## Core entities (domain objects + relations; ER-style
|
|
56
|
-
|
|
59
|
+
## Core entities (domain objects + relations; ER-style — when the runtime exposes an RBAC verb
|
|
60
|
+
taxonomy, derive the entity set from it: `X_CREATE/VIEW/EDIT/DELETE` ⇒ entity X)
|
|
61
|
+
## Integration surface (SDK/packages public API · REST/HTTP catalog · realtime/wire protocol — flag
|
|
62
|
+
where the app's OWN API differs from the published/developer API; that split is common)
|
|
57
63
|
## Auth & tenancy model (token type, where it lives, scope expression)
|
|
58
64
|
## Three-way API-path diff (the seam rule: bundle-declared vs session-observed vs wire-observed —
|
|
59
65
|
dormant/unshipped routes are findings)
|
|
@@ -123,6 +123,12 @@ flags**. To keep them consumable instead of triplicated:
|
|
|
123
123
|
in `data-model-api-surface.md`. A path that appears in `bundle` but never in `session`/`wire` is a
|
|
124
124
|
dormant/unshipped route — a finding, not noise.
|
|
125
125
|
|
|
126
|
+
> **Published API ≠ the app's own API.** A product's developer/REST/MCP surface (`api`/`docs`/`packages`
|
|
127
|
+
> dims) is frequently *not* the API its own app runs on (`session`/`bundle` dims) — different protocol,
|
|
128
|
+
> host, and operation set. Always diff the two: operations the app uses but the public API omits, **and**
|
|
129
|
+
> public operations the app never calls, are both findings. The published surface is the integration
|
|
130
|
+
> story; the runtime surface is the real architecture.
|
|
131
|
+
|
|
126
132
|
---
|
|
127
133
|
|
|
128
134
|
## 7. Ethics & redaction (non-negotiable — binds EVERY dimension)
|
|
@@ -137,6 +143,9 @@ Carried from the source playbook (`setup.md` §6). Violating them voids the work
|
|
|
137
143
|
6. **No bypassing safety harnesses** — redact, never evade.
|
|
138
144
|
7. **No exfiltration.** Captures stay local under `research/<target>/`.
|
|
139
145
|
8. **Respect TOS & robots.** For spec-writing / integration / competitive analysis — not credential theft, rate-limit bypass, or scraping.
|
|
146
|
+
9. **One browser-driving dimension at a time.** Chrome MCP is a shared singleton. Only the gated `session`/`wire-capture` dimensions drive the browser; parallel non-browser dumpers use WebFetch/curl, or — if a page genuinely needs a real browser — create their **own** tab and never touch the session tab. Never run two browser-driving dumpers concurrently.
|
|
147
|
+
10. **Verify an absence before asserting it.** A "host is NXDOMAIN / dimension is absent / endpoint 404s everywhere" claim that would *contradict another dimension* must be re-checked by a **second method** (alternate resolver, direct HTTP probe, second source) before it is written as fact. A single-resolver/single-probe negative is recorded as *tentative*, never as a cross-dimension directive. (An HTTP 4xx/5xx means the host **resolves** — present, not absent.)
|
|
148
|
+
11. **Bounded live-token use (read-only, in-page only).** To fill a Pass-1 capture gap you MAY read the live token from its store and replay a **read-only** request with it — *only* if the token is used **in-page / in-process only**, is **never written to `raw/`, never returned, never logged**, and the replay changes no state. This is the single permitted touch of a live credential; rules 2–3 (shape-not-values, never-save-secrets) still bind everything written to disk.
|
|
140
149
|
|
|
141
150
|
### 7.1 wire-capture & active-interception ethics (the strictest tier)
|
|
142
151
|
|
|
@@ -201,7 +210,11 @@ project, conversation, account, order), mine it first. Hit every list/state/hist
|
|
|
201
210
|
the existing token; capture verbatim bodies. This yields ~70% of the spec for free. Mine, by role:
|
|
202
211
|
identity (`/me`, `/account`, `/workspaces`), list (the canonical taxonomy), single-resource state
|
|
203
212
|
(server-injected vs client-sent fields), history/event-log (a whole trajectory in one call),
|
|
204
|
-
runtime/preview state, per-resource billing, account billing, feature flags/cohort,
|
|
213
|
+
runtime/preview state, per-resource billing, account billing, feature flags/cohort, **the account's
|
|
214
|
+
`entitlements`/`permissions` arrays** (the single richest data-model artifact — `entitlements`/`features`
|
|
215
|
+
enumerate every gated feature *including ones your plan hides*; `permissions` are CRUD-verbs-per-entity,
|
|
216
|
+
so the verb set enumerates the domain entity set — `X_CREATE/VIEW/EDIT/DELETE` ⇒ entity `X`; mine these
|
|
217
|
+
first), integration
|
|
205
218
|
catalog (`/mcp/`, `/integrations/`), and schema/capabilities (`/openapi.json`, `/.well-known/*`,
|
|
206
219
|
`/page-metadata`).
|
|
207
220
|
|
|
@@ -222,24 +235,59 @@ The same phases apply whether the target is an AI builder, analytics dashboard,
|
|
|
222
235
|
|
|
223
236
|
| Phase | Focus |
|
|
224
237
|
|-------|-------|
|
|
225
|
-
| **P0 Surface** | routes, bundles, storage shape, the API host(s), the auth tell |
|
|
226
|
-
| **P1 Auth** | token type + issuer + storage location + refresh path; tenancy model |
|
|
238
|
+
| **P0 Surface** | routes, bundles, storage shape, the API host(s), the auth tell; build the **coverage checklist** from any route/op catalog a sibling declared (§9.4) |
|
|
239
|
+
| **P1 Auth** | token type + issuer + storage location + refresh path; tenancy model; **disambiguate the transport by omission** (replay one authed read with each credential mechanism dropped) — and never test auth with a schema-only field |
|
|
227
240
|
| **P2 Core action** | the primary write — the submit/create body and its server resolution |
|
|
228
|
-
| **P3 Streaming** | the realtime transport (WS / SSE / WebChannel / Pusher / none) + event catalog |
|
|
241
|
+
| **P3 Streaming** | the realtime transport (WS / SSE / WebChannel / Pusher / none) + event catalog; **fingerprint binary frames by signature, never log them as opaque** (§9.4) |
|
|
229
242
|
| **P4 Runtime/state** | sandbox/preview/build/deploy state; the live result surface |
|
|
230
243
|
| **P5 Iteration** | the follow-up/edit endpoint (often surprisingly named) + any auto-iteration loop |
|
|
231
244
|
| **P6 Synthesis** | reconcile into the dimension's `_summary.md` + `captures/` + `curls.md` |
|
|
232
245
|
|
|
233
246
|
### 9.4 Runtime taps (Pass-1 instrumentation, run in-page)
|
|
234
247
|
|
|
248
|
+
> **Install the in-page interceptor FIRST, then move section-to-section WITHOUT a full reload.**
|
|
249
|
+
> CDP `read_network_requests` is unreliable for an already-loaded single-page app: it starts tracking
|
|
250
|
+
> only when first called and silently misses requests already in flight or served from cache (seen on a
|
|
251
|
+
> real run where only a third-party analytics beacon was captured while the app's own API calls went
|
|
252
|
+
> unseen). Treat it as a *supplement*; the in-page interceptor is the primary capture. A full-page
|
|
253
|
+
> `navigate` wipes the interceptor, so traverse the app using whatever it exposes — **clicking in-app
|
|
254
|
+
> nav works on any stack**; or call the client router if one is reachable (`history.pushState`+`popstate`,
|
|
255
|
+
> React Router / Vue Router / Angular Router, or a globally-exposed router such as `window.next.router`
|
|
256
|
+
> on Next.js). The principle is framework-agnostic; the specific call is just whatever that app ships.
|
|
257
|
+
|
|
235
258
|
- **WebSocket / EventSource / fetch+XHR interceptors** — wrap the constructors/methods to log
|
|
236
259
|
URLs, frames, and bodies (shapes, not secrets — redact in the wrapper before anything reaches
|
|
237
260
|
disk). Add a `SCRUB(value)` step inside the fetch wrapper so credentials never reach the harness.
|
|
261
|
+
**Never log a binary frame as opaque `<binary>`.** Any realtime/streaming/collab layer (token
|
|
262
|
+
streams, CRDT/OT edits, presence, telemetry, game or market state) may ship `ArrayBuffer`/`Blob`
|
|
263
|
+
frames; for each *distinct* frame capture its **byte-length + a hex dump of the first ~16 bytes** —
|
|
264
|
+
enough to fingerprint the wire format (protobuf, MessagePack, CBOR, Yjs/CRDT, gRPC-web, or a
|
|
265
|
+
text/topic-prefixed channel protocol) without recording content. **Decode the signature as ASCII
|
|
266
|
+
first** — many "binary" protocols are framed text (STOMP, Phoenix, Centrifugo, a `+topic` channel
|
|
267
|
+
prefix), so a legible prefix names the protocol in one step before a binary-codec guess. The frame
|
|
268
|
+
*signature* is the finding; the payload is not.
|
|
238
269
|
- **Storage shape inspector** — enumerate localStorage / sessionStorage / cookies / IndexedDB
|
|
239
270
|
**key names + lengths**, never raw values.
|
|
240
271
|
- **JWT decoder** — decode the token *structure* (alg, claim names, exp) with identity claims
|
|
241
272
|
(`sub`, `email`, `phone`, `user_id`, `uid`, `sid`) redacted.
|
|
242
|
-
-
|
|
273
|
+
- **Auth-channel disambiguation.** Token *location* is itself a tell: not in localStorage ⇒ an httpOnly
|
|
274
|
+
cookie **or** an SDK store (IndexedDB — Firebase/Auth0/Supabase/Cognito keep tokens there, which is why
|
|
275
|
+
a "no JWT in localStorage" result is a finding, not a dead end). Determine the *required* transport by
|
|
276
|
+
**omission**: replay one harmless authed read — using a field that needs the authed context (`viewer`/
|
|
277
|
+
`me`), **never** a schema-only field (GraphQL `__typename`, a health route) that resolves
|
|
278
|
+
unauthenticated — with each candidate credential dropped (cookie-only, then header-only); whichever
|
|
279
|
+
response comes back unauthorized names the channel the app actually requires.
|
|
280
|
+
- If a realtime SDK is loaded (Pusher, Ably, Socket.IO, Centrifugo, Phoenix, …), bind a global listener
|
|
281
|
+
to capture the event/channel catalog.
|
|
282
|
+
- **Third-party-SDK fingerprint** (cross-checks `infra-backend-fingerprint`). Storage key names + the
|
|
283
|
+
network host list name the embedded vendor stack — auth (Firebase/Auth0), flags (LaunchDarkly/Statsig),
|
|
284
|
+
support (Intercom/Zendesk/Ada), analytics (Segment/Amplitude/Datadog), payments (Stripe). Record them;
|
|
285
|
+
they corroborate the sub-processor list *independently* (a different dimension → promotes confidence).
|
|
286
|
+
- **Drive op coverage from any catalog a sibling declared.** If `deployed-client-bundle` (or `api`/`docs`)
|
|
287
|
+
recovered a declared **route / endpoint / operation catalog** — a REST path table, a GraphQL operation
|
|
288
|
+
set, an RPC/tool method list — consume it as a **coverage checklist**: walk the routes/actions that
|
|
289
|
+
trigger the un-observed entries so the declared-vs-observed diff (§6) reflects a real walk, not just the
|
|
290
|
+
landing page.
|
|
243
291
|
|
|
244
292
|
### 9.5 The capture contract (richer than a standard dimension dump)
|
|
245
293
|
|
|
@@ -254,6 +302,10 @@ Under `research/<target>/dimensions/session/`:
|
|
|
254
302
|
redacted) + `_decoded_fields`. Digest bodies > 50 KB into a structured summary.
|
|
255
303
|
- **`curls.md`** — reproducible curl per endpoint, the auth model documented, a variables block,
|
|
256
304
|
and an endpoint-catalog table.
|
|
305
|
+
- **`captures/screens/`** (recommended) — one annotated screenshot per primary surface (each major nav
|
|
306
|
+
section), plus a short tour recording if useful. The fastest "what does it actually do" artifact for a
|
|
307
|
+
human reader, complementing the wire teardown — and the place the visual product map lives. Redact any
|
|
308
|
+
visible PII before saving.
|
|
257
309
|
- **`_summary.md`** — the standard dimension bridge file (provenance frontmatter per §2) pointing
|
|
258
310
|
into the above, so Evaluation treats session like any other dimension. `method: session-playbook`,
|
|
259
311
|
`confidence: low` for Pass-1-only/inferred.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nurix/etna",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
4
4
|
"description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|