@nurix/etna 0.15.4 → 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 -1
- package/data/harnesses/re/CLAUDE.md +9 -2
- package/data/harnesses/re/agents/infra-backend-fingerprint.md +9 -0
- package/data/harnesses/re/harness.json +1 -1
- 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
|
@@ -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.
|
|
@@ -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": {
|