@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 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": "fcd26215a41d",
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
 
@@ -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": "790ded87e841"
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
 
@@ -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, integration
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
- - If a Pusher/realtime SDK is loaded, bind a global listener to capture the event catalog.
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.4",
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": {