@nurix/etna 0.15.4 → 0.15.6

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 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
- console.log(`\nInstalling ${pluginName} into ${targetDir}`);
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) console.log(` copied ${count} ${sub} → ${targetDir}/.claude/${sub}/`);
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 meta = readJson(harnessJsonDest);
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. Open Claude Code in ${targetDir}.`);
230
- console.log(`Re-run \`npx @nurix/etna ${pluginName}\` anytime to refresh harness content.\n`);
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) => {
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "be67d356d0f6",
2
+ "fingerprint": "ef554a39d069",
3
3
  "plugins": {
4
4
  "code": "code",
5
5
  "kb-agi": "kb-agi",
@@ -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
 
@@ -27,6 +27,12 @@ the endpoints it will call, and the flags it gates behind.
27
27
  pull every `<script src>` and `<link rel=modulepreload>`. Vite/Next ship hashed
28
28
  `/assets/*.js` and `/_next/static/chunks/*.js`. Grab the entry chunk + the largest vendor
29
29
  chunk; `curl -s <chunk-url> -o raw/.work/<name>.js`. Note `Content-Length` per chunk.
30
+ **Bound this — do NOT exhaustively fetch every lazy chunk.** The route table and the GraphQL/op
31
+ registry live in the entry + `_app` + a registry chunk; grab those plus the largest few vendor
32
+ chunks (cap ~40 chunks / ~5 MB / 90 s), and record the rest as a `gap`. Fetching all lazy chunks
33
+ (often 90+, multi-MB) can run 3–4× a sibling dumper and read as hung. **Per §8, write a
34
+ `status: partial` `_summary.md` from the entry/manifest chunks BEFORE the wider fetch loop**, then
35
+ enrich and flip to `complete`.
30
36
  2. **Check for source maps FIRST.** Grep each chunk for `//# sourceMappingURL=`. If present,
31
37
  fetch the `.map` and reassemble original sources (`npx source-map-explorer` or unpack
32
38
  `sourcesContent` from the JSON map). A live map flips you to `method: source-map-reassembly,
@@ -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.
@@ -2,5 +2,5 @@
2
2
  "name": "re",
3
3
  "version": "0.1.2",
4
4
  "epoch": null,
5
- "digest": "f5dda6cb5667"
5
+ "digest": "29a864bc5245"
6
6
  }
@@ -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
- ## Integration surface (SDK/packages public API · REST/HTTP catalog · realtime/wire protocol)
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
 
@@ -170,6 +179,15 @@ A dump is complete when: `_summary.md` exists with full provenance frontmatter +
170
179
  every Finding is backed by a `raw/` artifact or inline value; `raw/` artifacts have `_meta` +
171
180
  decoding (large ones digested); redaction pass run; `gaps` recorded (never fabricated).
172
181
 
182
+ **Summary-first + bounded, visible fan-out (never go silent long enough to look dead).** Write a
183
+ `status: partial` `_summary.md` with whatever is mined so far **before** any long-tail fetch loop
184
+ (downloading many chunks, crawling many pages, deep pagination), then enrich it in place and flip to
185
+ `complete`. **Bound** every fan-out — cap it by count / total bytes / wall-time (e.g. ≤40 items or
186
+ ≤90 s) and record the remainder as a `gap` rather than fetching exhaustively; emit a progress note per
187
+ batch. A dumper that fetches everything before writing anything can run many times longer than its
188
+ siblings and read as hung to the orchestrator, forcing a needless salvage. Partial-but-finalized beats
189
+ complete-but-silent.
190
+
173
191
  ---
174
192
 
175
193
  ## 9. The session dimension — logged-in browser-session playbook
@@ -201,7 +219,11 @@ project, conversation, account, order), mine it first. Hit every list/state/hist
201
219
  the existing token; capture verbatim bodies. This yields ~70% of the spec for free. Mine, by role:
202
220
  identity (`/me`, `/account`, `/workspaces`), list (the canonical taxonomy), single-resource state
203
221
  (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, integration
222
+ runtime/preview state, per-resource billing, account billing, feature flags/cohort, **the account's
223
+ `entitlements`/`permissions` arrays** (the single richest data-model artifact — `entitlements`/`features`
224
+ enumerate every gated feature *including ones your plan hides*; `permissions` are CRUD-verbs-per-entity,
225
+ so the verb set enumerates the domain entity set — `X_CREATE/VIEW/EDIT/DELETE` ⇒ entity `X`; mine these
226
+ first), integration
205
227
  catalog (`/mcp/`, `/integrations/`), and schema/capabilities (`/openapi.json`, `/.well-known/*`,
206
228
  `/page-metadata`).
207
229
 
@@ -222,24 +244,59 @@ The same phases apply whether the target is an AI builder, analytics dashboard,
222
244
 
223
245
  | Phase | Focus |
224
246
  |-------|-------|
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 |
247
+ | **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) |
248
+ | **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
249
  | **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 |
250
+ | **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
251
  | **P4 Runtime/state** | sandbox/preview/build/deploy state; the live result surface |
230
252
  | **P5 Iteration** | the follow-up/edit endpoint (often surprisingly named) + any auto-iteration loop |
231
253
  | **P6 Synthesis** | reconcile into the dimension's `_summary.md` + `captures/` + `curls.md` |
232
254
 
233
255
  ### 9.4 Runtime taps (Pass-1 instrumentation, run in-page)
234
256
 
257
+ > **Install the in-page interceptor FIRST, then move section-to-section WITHOUT a full reload.**
258
+ > CDP `read_network_requests` is unreliable for an already-loaded single-page app: it starts tracking
259
+ > only when first called and silently misses requests already in flight or served from cache (seen on a
260
+ > real run where only a third-party analytics beacon was captured while the app's own API calls went
261
+ > unseen). Treat it as a *supplement*; the in-page interceptor is the primary capture. A full-page
262
+ > `navigate` wipes the interceptor, so traverse the app using whatever it exposes — **clicking in-app
263
+ > nav works on any stack**; or call the client router if one is reachable (`history.pushState`+`popstate`,
264
+ > React Router / Vue Router / Angular Router, or a globally-exposed router such as `window.next.router`
265
+ > on Next.js). The principle is framework-agnostic; the specific call is just whatever that app ships.
266
+
235
267
  - **WebSocket / EventSource / fetch+XHR interceptors** — wrap the constructors/methods to log
236
268
  URLs, frames, and bodies (shapes, not secrets — redact in the wrapper before anything reaches
237
269
  disk). Add a `SCRUB(value)` step inside the fetch wrapper so credentials never reach the harness.
270
+ **Never log a binary frame as opaque `<binary>`.** Any realtime/streaming/collab layer (token
271
+ streams, CRDT/OT edits, presence, telemetry, game or market state) may ship `ArrayBuffer`/`Blob`
272
+ frames; for each *distinct* frame capture its **byte-length + a hex dump of the first ~16 bytes** —
273
+ enough to fingerprint the wire format (protobuf, MessagePack, CBOR, Yjs/CRDT, gRPC-web, or a
274
+ text/topic-prefixed channel protocol) without recording content. **Decode the signature as ASCII
275
+ first** — many "binary" protocols are framed text (STOMP, Phoenix, Centrifugo, a `+topic` channel
276
+ prefix), so a legible prefix names the protocol in one step before a binary-codec guess. The frame
277
+ *signature* is the finding; the payload is not.
238
278
  - **Storage shape inspector** — enumerate localStorage / sessionStorage / cookies / IndexedDB
239
279
  **key names + lengths**, never raw values.
240
280
  - **JWT decoder** — decode the token *structure* (alg, claim names, exp) with identity claims
241
281
  (`sub`, `email`, `phone`, `user_id`, `uid`, `sid`) redacted.
242
- - If a Pusher/realtime SDK is loaded, bind a global listener to capture the event catalog.
282
+ - **Auth-channel disambiguation.** Token *location* is itself a tell: not in localStorage ⇒ an httpOnly
283
+ cookie **or** an SDK store (IndexedDB — Firebase/Auth0/Supabase/Cognito keep tokens there, which is why
284
+ a "no JWT in localStorage" result is a finding, not a dead end). Determine the *required* transport by
285
+ **omission**: replay one harmless authed read — using a field that needs the authed context (`viewer`/
286
+ `me`), **never** a schema-only field (GraphQL `__typename`, a health route) that resolves
287
+ unauthenticated — with each candidate credential dropped (cookie-only, then header-only); whichever
288
+ response comes back unauthorized names the channel the app actually requires.
289
+ - If a realtime SDK is loaded (Pusher, Ably, Socket.IO, Centrifugo, Phoenix, …), bind a global listener
290
+ to capture the event/channel catalog.
291
+ - **Third-party-SDK fingerprint** (cross-checks `infra-backend-fingerprint`). Storage key names + the
292
+ network host list name the embedded vendor stack — auth (Firebase/Auth0), flags (LaunchDarkly/Statsig),
293
+ support (Intercom/Zendesk/Ada), analytics (Segment/Amplitude/Datadog), payments (Stripe). Record them;
294
+ they corroborate the sub-processor list *independently* (a different dimension → promotes confidence).
295
+ - **Drive op coverage from any catalog a sibling declared.** If `deployed-client-bundle` (or `api`/`docs`)
296
+ recovered a declared **route / endpoint / operation catalog** — a REST path table, a GraphQL operation
297
+ set, an RPC/tool method list — consume it as a **coverage checklist**: walk the routes/actions that
298
+ trigger the un-observed entries so the declared-vs-observed diff (§6) reflects a real walk, not just the
299
+ landing page.
243
300
 
244
301
  ### 9.5 The capture contract (richer than a standard dimension dump)
245
302
 
@@ -254,6 +311,10 @@ Under `research/<target>/dimensions/session/`:
254
311
  redacted) + `_decoded_fields`. Digest bodies > 50 KB into a structured summary.
255
312
  - **`curls.md`** — reproducible curl per endpoint, the auth model documented, a variables block,
256
313
  and an endpoint-catalog table.
314
+ - **`captures/screens/`** (recommended) — one annotated screenshot per primary surface (each major nav
315
+ section), plus a short tour recording if useful. The fastest "what does it actually do" artifact for a
316
+ human reader, complementing the wire teardown — and the place the visual product map lives. Redact any
317
+ visible PII before saving.
257
318
  - **`_summary.md`** — the standard dimension bridge file (provenance frontmatter per §2) pointing
258
319
  into the above, so Evaluation treats session like any other dimension. `method: session-playbook`,
259
320
  `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.4",
3
+ "version": "0.15.6",
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": {