rewritable 0.13.0 → 0.15.0

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/README.md CHANGED
@@ -261,6 +261,19 @@ rwa host notes.html --url https://host.example --json # {"id":"…","token":"
261
261
 
262
262
  This command is **network-bearing** (like `rwa clone` / `rwa publish-site`), so the offline-first rule does not apply to it.
263
263
 
264
+ ### `rwa intelligence new <role>`
265
+
266
+ Mint a **droppable intelligence** — a signed `rwa-agent/1` role packaged as a *carrier* rewritable you can drop onto another rewritable to retune its ⌘K editor (intelligence/0.2). It generates a fresh Ed25519 keypair, signs the role, and scaffolds a self-describing `skill-host` carrier. The **private key** is written to a sibling `<name>.key.json`, kept out of the carrier — keep it to publish updates under the same author identity.
267
+
268
+ ```
269
+ rwa intelligence new concise \
270
+ --prompt "Tighten prose: shorter sentences, fewer hedges, meaning preserved." \
271
+ --model anthropic/claude-sonnet-4-6 --backend openrouter \
272
+ --affinity document,presentation
273
+ ```
274
+
275
+ Flags: `--prompt` (required — the role's system prompt), `--description`, `--model` / `--backend` (a *recommended* model offered on activation behind consent — never auto-applied, never carries your key), `--affinity` (comma-separated document kinds; advisory — a mismatch only warns), `--vault` (comma-separated namespaces the role may reach), `--out <path>`, `--force`. Offline; the carrier holds only the public key + signature.
276
+
264
277
  ### `rwa skin <path> <name>`
265
278
 
266
279
  Pick a **named look** for a rewritable instead of hand-styling it from the blank lens. A skin is one self-contained `<style data-rwa-skin="NAME">` block — system fonts only, no web fonts or remote assets — that the command splices into the **document body**. So it commits with the document, ships inside the exported `.html`, survives sharing, and one in-browser undo (`⌘Z`) reverts it. Five presets ship today: `notion-clean`, `linear-dark`, `editorial-serif`, `stripe-docs`, `terminal-mono` (clean · dark · editorial · docs · terminal).
package/bin/rwa.mjs CHANGED
@@ -728,6 +728,36 @@ function detectProductKind(fileText) {
728
728
  // `rwa skill publish <file.rwa-skill.json> [--url base] [--json]` — publish a SIGNED skill
729
729
  // envelope to the marketplace index (POST /skills/publish, I6 §11). The envelope is already
730
730
  // signed (no key needed). Online by design; exit 4 labeled `publish_error` (like `publish`).
731
+ // `rwa intelligence new <role> --prompt "..." [--description ..] [--model id] [--backend name]
732
+ // [--affinity kind,kind] [--vault ns,ns] [--out file] [--force]` — I-C (intelligence/0.2 §6):
733
+ // mint a signed rwa-agent/1 role and scaffold a carrier rewritable (private key → sibling file).
734
+ if (verb === 'intelligence') {
735
+ const sub = rest[0];
736
+ if (sub !== 'new') {
737
+ process.stderr.write("rwa intelligence: unknown subcommand '" + (sub || '') + "' (try: rwa intelligence new <role> --prompt \"...\")\n");
738
+ process.exitCode = 1;
739
+ return;
740
+ }
741
+ const subRest = rest.slice(1);
742
+ const valFlags = ['--prompt', '--description', '--model', '--backend', '--affinity', '--vault', '--out'];
743
+ const role = subRest.find((a, i) => !a.startsWith('-') && !valFlags.includes(subRest[i - 1]));
744
+ const g = (n) => getFlag(n, subRest).value;
745
+ const list = (n) => { const v = g(n); return v ? v.split(',').map(s => s.trim()).filter(Boolean) : []; };
746
+ try {
747
+ const { intelligenceNewCmd } = await import('../src/intelligence.mjs');
748
+ await intelligenceNewCmd({
749
+ role, prompt: g('--prompt'), description: g('--description'),
750
+ model: g('--model'), backend: g('--backend'),
751
+ affinity: list('--affinity'), vault: list('--vault'),
752
+ outPath: g('--out'), force: subRest.includes('--force') || subRest.includes('-f'),
753
+ });
754
+ } catch (e) {
755
+ process.stderr.write('rwa intelligence: ' + ((e && e.message) || e) + '\n');
756
+ process.exitCode = (e && e.exitCode) || 1;
757
+ }
758
+ return;
759
+ }
760
+
731
761
  if (verb === 'skill') {
732
762
  const sub = rest[0];
733
763
  const subRest = rest.slice(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rewritable",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "CLI for re-writeable: emit and import single-file rwa documents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2326,13 +2326,23 @@ async function renderActionsModePanel(panel) {
2326
2326
  // recommended model (intelligence/0.2 I-A). A verified role can be activated; unverified cannot.
2327
2327
  const agentList = (typeof runtimeListAgents === 'function') ? runtimeListAgents() : [];
2328
2328
  const activeRole = (runtimeAgentActive() || {}).role || null;
2329
+ const advisorSet = (typeof runtimeListAdvisors === 'function') ? runtimeListAdvisors() : [];
2330
+ const advisorFull = advisorSet.length >= 3;
2329
2331
  const agentRows = agentList.length ? agentList.map(a => {
2330
2332
  const isActive = a.role === activeRole;
2331
- const meta = (a.verified ? 'verified' : 'unverified') + (isActive ? ' · active' : '');
2332
- const btn = !a.verified ? '' : (isActive
2333
- ? '<button type="button" data-agent-off="1">Deactivate</button>'
2334
- : '<button type="button" data-agent-on="' + escRuntimeHtml(a.role) + '">Activate</button>');
2335
- return '<div class="rwa-mode-row"><div><div class="rwa-mode-title">' + escRuntimeHtml(a.role) + '</div><div class="rwa-mode-meta">' + escRuntimeHtml('intelligence · ' + meta) + '</div></div>' + btn + '</div>';
2333
+ const isAdvisor = advisorSet.indexOf(a.role) >= 0;
2334
+ const state = isActive ? 'primary' : (isAdvisor ? 'advisor' : '');
2335
+ const aff = (a.affinity && a.affinity.length) ? ' · for ' + a.affinity.join('/') : '';
2336
+ const meta = (a.verified ? 'verified' : 'unverified') + (state ? ' · ' + state : '') + aff;
2337
+ let btns = '';
2338
+ if (a.verified) {
2339
+ const r = escRuntimeHtml(a.role);
2340
+ if (isActive) btns = '<button type="button" data-agent-off="1">Deactivate</button>';
2341
+ else if (isAdvisor) btns = '<button type="button" data-agent-advoff="' + r + '">Remove advisor</button>';
2342
+ else btns = '<button type="button" data-agent-on="' + r + '">Activate</button>'
2343
+ + (advisorFull ? '<button type="button" disabled title="advisor limit reached">Add advisor</button>' : '<button type="button" data-agent-advon="' + r + '">Add advisor</button>');
2344
+ }
2345
+ return '<div class="rwa-mode-row"><div><div class="rwa-mode-title">' + escRuntimeHtml(a.role) + '</div><div class="rwa-mode-meta">' + escRuntimeHtml('intelligence · ' + meta) + '</div></div>' + btns + '</div>';
2336
2346
  }).join('') : '<div class="rwa-mode-empty">No intelligences installed.</div>';
2337
2347
  panel.innerHTML = [
2338
2348
  '<div class="rwa-mode-section">',
@@ -2366,12 +2376,20 @@ async function renderActionsModePanel(panel) {
2366
2376
  btn.addEventListener('click', () => runtimeSetMode('skills'));
2367
2377
  });
2368
2378
  panel.querySelectorAll('[data-agent-on]').forEach(btn => btn.addEventListener('click', () => {
2369
- try { runtimeActivateAgent(btn.getAttribute('data-agent-on')); } // setActive + offer recommended model
2379
+ const role = btn.getAttribute('data-agent-on');
2380
+ try { runtimeActivateAgent(role); const aw = affinityWarning(role); if (aw && typeof setStatus === 'function') setStatus('', aw); } // setActive + offer model; advisory affinity warn (I-D)
2370
2381
  catch (e) { if (typeof setStatus === 'function') setStatus('err', '✗ ' + (e && (e.code || e.message))); }
2371
2382
  renderActionsModePanel(panel);
2372
2383
  }));
2373
2384
  const agentOff = panel.querySelector('[data-agent-off]');
2374
2385
  if (agentOff) agentOff.addEventListener('click', () => { runtimeSetActiveAgent(null); renderActionsModePanel(panel); });
2386
+ panel.querySelectorAll('[data-agent-advon]').forEach(btn => btn.addEventListener('click', () => {
2387
+ const role = btn.getAttribute('data-agent-advon');
2388
+ try { runtimeAddAdvisor(role); const aw = affinityWarning(role); if (aw && typeof setStatus === 'function') setStatus('', aw); } // I-E: layer an advisory lens; I-D affinity warn
2389
+ catch (e) { if (typeof setStatus === 'function') setStatus('err', '✗ ' + (e && (e.code || e.message))); }
2390
+ renderActionsModePanel(panel);
2391
+ }));
2392
+ panel.querySelectorAll('[data-agent-advoff]').forEach(btn => btn.addEventListener('click', () => { runtimeRemoveAdvisor(btn.getAttribute('data-agent-advoff')); renderActionsModePanel(panel); }));
2375
2393
  }
2376
2394
 
2377
2395
  // ─── Agent (rwa-edit/1) ─────────────────────────────────────────────
@@ -5613,11 +5631,26 @@ function getActiveActor() {
5613
5631
  // system_prompt swaps the per-kind FRAMING; the shared SYSTEM_PROMPT_RULES (tool protocol, frozen-
5614
5632
  // zone rules, data-rwa-id) are always appended so editing still works. No agent → the singleton.
5615
5633
  function resolveSystemPrompt() {
5634
+ let base = SYSTEM_PROMPT;
5616
5635
  if (activeAgentRole) {
5617
5636
  const rec = Array.from(installedAgents.values()).find(a => a.role === activeAgentRole && a.verified);
5618
- if (rec && typeof rec.manifest.system_prompt === 'string') return rec.manifest.system_prompt + '\n' + SYSTEM_PROMPT_RULES;
5637
+ if (rec && typeof rec.manifest.system_prompt === 'string') base = rec.manifest.system_prompt + '\n' + SYSTEM_PROMPT_RULES;
5638
+ }
5639
+ return base + _agAdvisorBlock(); // I-E: append any advisory lenses (secondary; empty → byte-identical)
5640
+ }
5641
+ // I-E (intelligence/0.2 §6) — blended overlays. The PRIMARY is activeAgentRole (framing/actor/vault,
5642
+ // unchanged). advisorRoles add advisory PROSE only — never capabilities, so the vault gate
5643
+ // (_agVaultAllowed, keyed on the active record) stays primary-only by construction. Verified-only,
5644
+ // ephemeral (never serialized), capped. Block is omitted when empty (single-role prompt unchanged).
5645
+ function _agAdvisorBlock() {
5646
+ if (!advisorRoles.size) return '';
5647
+ const lines = [];
5648
+ for (const role of advisorRoles) {
5649
+ if (role === activeAgentRole) continue; // a role is primary XOR advisor, never both
5650
+ const rec = Array.from(installedAgents.values()).find(a => a.role === role && a.verified);
5651
+ if (rec && typeof rec.manifest.system_prompt === 'string') lines.push('- ' + role + ': ' + rec.manifest.system_prompt);
5619
5652
  }
5620
- return SYSTEM_PROMPT;
5653
+ return lines.length ? '\n\nAdditional advisory lenses (secondary — apply only where they don\'t conflict with the above):\n' + lines.join('\n') : '';
5621
5654
  }
5622
5655
  // I12 — the role-scoped vault gate: exact vault:<ns> membership in the agent's vault_namespace_set
5623
5656
  // (mirrors _skVaultAllowed against the agent record instead of the skill's permissions).
@@ -6832,6 +6865,10 @@ const installedSkills = new Map();
6832
6865
  // starts with no agent active per §12).
6833
6866
  const installedAgents = new Map();
6834
6867
  let activeAgentRole = null;
6868
+ // I-E — advisory roles layered atop the primary (in-memory, ephemeral, never serialized — like the
6869
+ // active role). Capped for prompt coherence/size. Advisors contribute prose only (see _agAdvisorBlock).
6870
+ const advisorRoles = new Set();
6871
+ const ADVISOR_CAP = 3;
6835
6872
  // I8 (v0.9 §9) — hook firing state. activeHooks holds skillIds currently executing (re-entrancy
6836
6873
  // guard, Inv 23); _lastHookMode tracks the prior mode for on-mode-change's `previous`; _hookSeq
6837
6874
  // makes rwa_hook_log keys unique within a millisecond.
@@ -7810,7 +7847,7 @@ function _agAgentsRegion() {
7810
7847
  };
7811
7848
  }
7812
7849
  function runtimeListAgents() {
7813
- return Array.from(installedAgents.values()).map(a => ({ role: a.role, author_pubkey: a.manifest && a.manifest.author_pubkey, verified: a.verified }));
7850
+ return Array.from(installedAgents.values()).map(a => ({ role: a.role, author_pubkey: a.manifest && a.manifest.author_pubkey, verified: a.verified, affinity: getAffinity(a.envelope) }));
7814
7851
  }
7815
7852
  function runtimeAgentActive() {
7816
7853
  if (!activeAgentRole) return null;
@@ -7826,6 +7863,19 @@ function runtimeSetActiveAgent(role) {
7826
7863
  if (!rec.verified) throw new RwaEditError('unverified_agent', null, { role });
7827
7864
  activeAgentRole = role;
7828
7865
  }
7866
+ // I-E — add/remove an advisory role. Verified-only (its prompt drives modify), not-the-primary,
7867
+ // capped. Checks order so unverified/not-found beat the cap. Returns the current advisor list.
7868
+ function runtimeAddAdvisor(role) {
7869
+ const rec = Array.from(installedAgents.values()).find(a => a.role === role);
7870
+ if (!rec) throw new RwaEditError('agent_not_found', null, { role });
7871
+ if (!rec.verified) throw new RwaEditError('unverified_agent', null, { role });
7872
+ if (role === activeAgentRole || advisorRoles.has(role)) return Array.from(advisorRoles); // primary / already → no-op
7873
+ if (advisorRoles.size >= ADVISOR_CAP) throw new RwaEditError('advisor_cap_reached', null, { cap: ADVISOR_CAP });
7874
+ advisorRoles.add(role);
7875
+ return Array.from(advisorRoles);
7876
+ }
7877
+ function runtimeRemoveAdvisor(role) { advisorRoles.delete(role); return Array.from(advisorRoles); }
7878
+ function runtimeListAdvisors() { return Array.from(advisorRoles); }
7829
7879
  // §12 — validate + verify + register an agent, then persist the #rwa-agents zone (same discipline as
7830
7880
  // runtimeInstallSkill: rollback the in-memory map on a persist failure). A signed-but-unverified
7831
7881
  // agent registers (verified:false) but can't activate.
@@ -7851,6 +7901,7 @@ async function runtimeUninstallAgent(agentId) {
7851
7901
  if (!prev) return { ok: false, errors: ['not_installed'] };
7852
7902
  installedAgents.delete(agentId);
7853
7903
  if (activeAgentRole && prev.role === activeAgentRole && !Array.from(installedAgents.values()).some(a => a.role === activeAgentRole)) activeAgentRole = null;
7904
+ if (prev.role && !Array.from(installedAgents.values()).some(a => a.role === prev.role)) advisorRoles.delete(prev.role); // I-E: drop a removed role from advisors
7854
7905
  try {
7855
7906
  await runtimeRegionCommit({ regions: [_agAgentsRegion()], actor: 'agent:uninstall', reachability: 'frozen' });
7856
7907
  } catch (e) {
@@ -8118,6 +8169,24 @@ function getRecommendation(envelope) {
8118
8169
  if (typeof e.recommended_backend === 'string' && REC_BACKENDS.includes(e.recommended_backend.trim())) out.backend = e.recommended_backend.trim();
8119
8170
  return (out.model || out.backend) ? out : null;
8120
8171
  }
8172
+ // I-D (intelligence/0.2 §6) — advisory kind-affinity. A role may declare an UNSIGNED `affinity`
8173
+ // envelope field (the document kinds it is tuned for). Activating/advising it on a mismatched
8174
+ // PRODUCT_KIND only WARNS — never blocks (spec §4: affinity is a soft note). Advisory by design.
8175
+ function getAffinity(envelope) {
8176
+ const a = envelope && envelope.affinity;
8177
+ if (Array.isArray(a)) return a.filter(x => typeof x === 'string' && x);
8178
+ if (typeof a === 'string' && a) return [a];
8179
+ return [];
8180
+ }
8181
+ function affinityWarning(role) {
8182
+ const rec = Array.from(installedAgents.values()).find(x => x.role === role);
8183
+ if (!rec) return null;
8184
+ const aff = getAffinity(rec.envelope);
8185
+ if (!aff.length || aff.indexOf(PRODUCT_KIND) >= 0) return null;
8186
+ return '⚠ “' + role + '” is tuned for ' + aff.join('/') + ' documents, but this is a ' + PRODUCT_KIND + ' — applied anyway.';
8187
+ }
8188
+ window.__rwaGetAffinity = getAffinity;
8189
+ window.__rwaAffinityWarning = affinityWarning;
8121
8190
  function applyRecommendation(rec) {
8122
8191
  const r = rec || {};
8123
8192
  const applied = {};
@@ -8161,6 +8230,8 @@ window.__rwaGetRecommendation = getRecommendation;
8161
8230
  window.__rwaApplyRecommendation = applyRecommendation;
8162
8231
  window.__rwaOfferRecommendedModel = offerRecommendedModel;
8163
8232
  window.__rwaActivateAgent = runtimeActivateAgent;
8233
+ window.__rwaResolveSystemPrompt = resolveSystemPrompt; // I-E test hook — the assembled modify() prompt
8234
+ window.__rwaGetActiveActor = getActiveActor;
8164
8235
 
8165
8236
  // §4/§5a — does a network: host pattern admit a host? Mirror of cli/src/skill-manifest.mjs
8166
8237
  // matchNetworkOrigin (keep in step). The bridge's per-call origin check.
@@ -9694,7 +9765,7 @@ document.addEventListener('keydown', e => {
9694
9765
  discoverSkills: runtimeDiscoverSkills, // v0.9 §11 (I6) — GET the marketplace index (opt-in network)
9695
9766
  fetchSkillFromIndex: runtimeFetchSkillFromIndex, // v0.9 §11 (I6) — fetch + client-side-verify an indexed skill
9696
9767
  vault: { get: runtimeVaultGet, set: runtimeVaultSet, has: runtimeVaultHas, namespaces: runtimeVaultNamespaces, unlock: runtimeVaultUnlock, lock: runtimeVaultLock, isLocked: runtimeVaultIsLocked, export: runtimeVaultExport, import: runtimeVaultImport }, // v0.8 §6 + v0.9 §14 (I13) portable export/import
9697
- agents: { list: runtimeListAgents, active: runtimeAgentActive, setActive: runtimeSetActiveAgent, activate: runtimeActivateAgent, install: runtimeInstallAgent, uninstall: runtimeUninstallAgent, message: runtimeAgentMessage, showInstallDialog: showAgentInstallDialog, offerModel: offerRecommendedModel }, // v0.9 §12 — multi-agent roles; `activate` = setActive + offer recommended model (intelligence/0.2 I-A)
9768
+ agents: { list: runtimeListAgents, active: runtimeAgentActive, setActive: runtimeSetActiveAgent, activate: runtimeActivateAgent, install: runtimeInstallAgent, uninstall: runtimeUninstallAgent, message: runtimeAgentMessage, showInstallDialog: showAgentInstallDialog, offerModel: offerRecommendedModel, addAdvisor: runtimeAddAdvisor, removeAdvisor: runtimeRemoveAdvisor, advisors: runtimeListAdvisors }, // v0.9 §12 — multi-agent roles; `activate` = setActive + offer recommended model (I-A); addAdvisor/removeAdvisor/advisors = blended overlays (I-E)
9698
9769
  hookLog: runtimeHookLog, // v0.9 §9 — the hook audit trail (rwa_hook_log)
9699
9770
  };
9700
9771
  // `status` is a getter so each read returns a fresh snapshot of
@@ -0,0 +1,79 @@
1
+ // I-C (intelligence/0.2 §6) — `rwa intelligence new <role>`: mint a signed rwa-agent/1 role and
2
+ // scaffold a CARRIER rewritable (a skill-host holding the record + a self-describing card). The
3
+ // carrier ships only the PUBLIC key + signature; the PRIVATE key is written to a sibling .key.json
4
+ // (keep it to publish updates under the same author identity). Offline; reuses the agent canon
5
+ // (skill-manifest) and the seed bootstrap (seed.mjs) — no new wire-type, no canon fork.
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import { webcrypto, randomUUID } from 'node:crypto';
9
+ import { SEED_CANDIDATES } from './commands.mjs';
10
+ import { loadSeed, applySeedSubs, kindOverrides, replaceInlineDoc } from './seed.mjs';
11
+ import { agentSigningMessage } from './skill-manifest.mjs';
12
+
13
+ const ROLE_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
14
+ const REC_MODEL_RE = /^[A-Za-z0-9._:\/-]{1,200}$/;
15
+ const REC_BACKENDS = ['openrouter', 'ollama', 'lmstudio', 'atomic', 'bridge', 'bridge-session'];
16
+ const b64 = (u8) => Buffer.from(u8).toString('base64');
17
+ const rel = (p) => path.relative(process.cwd(), p) || p;
18
+ const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
19
+ const fail = (msg, code = 2) => { const e = new Error(msg); e.exitCode = code; throw e; };
20
+
21
+ export async function intelligenceNewCmd(opts = {}) {
22
+ const role = opts.role, prompt = opts.prompt;
23
+ if (!role || !ROLE_RE.test(role)) fail('intelligence: <role> must be lowercase a-z0-9_- (≤64, leading alphanumeric)');
24
+ if (!prompt || typeof prompt !== 'string') fail('intelligence: --prompt "<system prompt>" is required');
25
+ if (prompt.includes('`') || prompt.includes('${') || /<\/?DOC>/i.test(prompt)) fail('intelligence: --prompt must not contain ` ${ or <DOC>');
26
+ if (opts.model != null && !REC_MODEL_RE.test(String(opts.model))) fail('intelligence: --model is not a valid model id');
27
+ if (opts.backend != null && !REC_BACKENDS.includes(String(opts.backend))) fail('intelligence: --backend must be one of ' + REC_BACKENDS.join('/'));
28
+ const vault = (opts.vault || []).map(v => /^vault:/.test(v) ? v : 'vault:' + v);
29
+ const affinity = (opts.affinity || []).filter(Boolean);
30
+
31
+ // Mint + sign the rwa-agent/1 record. The signature is over `agent` (the canon); the
32
+ // recommendation/affinity ride OUTSIDE it (unsigned envelope fields, per I-A/I-D).
33
+ const kp = await webcrypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']);
34
+ const author_pubkey = b64(new Uint8Array(await webcrypto.subtle.exportKey('raw', kp.publicKey)));
35
+ const agent = { author_pubkey, description: opts.description || ('The ' + role + ' role.'), role, system_prompt: prompt, vault_namespace_set: vault, version: 'rwa-agent/1' };
36
+ const signature = b64(new Uint8Array(await webcrypto.subtle.sign({ name: 'Ed25519' }, kp.privateKey, agentSigningMessage(agent))));
37
+ const envelope = { agent, signature };
38
+ if (opts.model) envelope.recommended_model = String(opts.model);
39
+ if (opts.backend) envelope.recommended_backend = String(opts.backend);
40
+ if (affinity.length) envelope.affinity = affinity;
41
+
42
+ // Scaffold the carrier — a skill-host bootstrap + card + frozen #rwa-agents zone.
43
+ const out = path.resolve(opts.outPath || ('./' + role + '.intelligence.html'));
44
+ if (!opts.force) { let exists = false; try { await fs.access(out); exists = true; } catch (_) {} if (exists) fail('intelligence: ' + rel(out) + ' exists (use --force)'); }
45
+ const seed = await loadSeed(SEED_CANDIDATES);
46
+ const ov = kindOverrides('skill-host');
47
+ let result = applySeedSubs(seed, { uuid: randomUUID(), title: 'Intelligence — ' + role, fileMeta: path.basename(out), productKind: 'skill-host', lensPlaceholder: ov.lensPlaceholder, palPlaceholder: ov.palPlaceholder, productHeader: ov.productHeader, lensClickToAnchor: ov.lensClickToAnchor });
48
+ const zone = '<div data-rwa-frozen id="rwa-agents"><script type="application/rwa-agent+json">' + b64(Buffer.from(JSON.stringify(envelope))) + '</script></div>';
49
+ result = replaceInlineDoc(result, buildCard({ role, prompt, model: opts.model, backend: opts.backend, affinity, vault }) + '\n' + zone);
50
+ await fs.writeFile(out, result, 'utf8');
51
+
52
+ // The private key — needed to re-sign updates under the same author identity. Sibling file, loud.
53
+ const fingerprint = Buffer.from(await webcrypto.subtle.digest('SHA-256', Buffer.from(author_pubkey, 'base64'))).toString('hex').slice(0, 16);
54
+ const keyOut = out.replace(/\.html?$/i, '') + '.key.json';
55
+ // 0600: the file holds the PRIVATE key — owner read/write only, never world-readable.
56
+ await fs.writeFile(keyOut, JSON.stringify({
57
+ role, author_pubkey, fingerprint,
58
+ private_key_pkcs8_b64: b64(new Uint8Array(await webcrypto.subtle.exportKey('pkcs8', kp.privateKey))),
59
+ warning: 'SECRET. Keep this to publish updates to this intelligence under the same author identity. Never commit or share it. The carrier .html holds only the public key.',
60
+ }, null, 2) + '\n', { mode: 0o600 });
61
+ try { await fs.chmod(keyOut, 0o600); } catch (_) {} // guarantee owner-only even if the file pre-existed (writeFile mode applies only on create; best-effort on non-POSIX)
62
+
63
+ console.log('wrote ' + rel(out) + ' (intelligence "' + role + '")');
64
+ console.log('author ' + fingerprint + ' — private key saved to ' + rel(keyOut) + ' (keep secret; needed to update this intelligence)');
65
+ return { out, keyOut, fingerprint, envelope };
66
+ }
67
+
68
+ function buildCard({ role, prompt, model, backend, affinity, vault }) {
69
+ const recLine = model ? '\n<li><strong>Recommended model:</strong> <code>' + esc(model) + '</code>' + (backend ? ' on <code>' + esc(backend) + '</code>' : '') + ' — offered on activation, behind consent (your session only; key untouched).</li>' : '';
70
+ const affLine = affinity.length ? '\n<li><strong>Affinity:</strong> ' + esc(affinity.join(', ')) + ' (advisory — a mismatch only warns).</li>' : '';
71
+ const vaultLine = '\n<li><strong>Vault namespaces:</strong> ' + (vault.length ? esc(vault.join(', ')) : 'none') + '.</li>';
72
+ return '<article>\n' +
73
+ '<h1>Intelligence — &ldquo;' + esc(role) + '&rdquo;</h1>\n' +
74
+ '<p class="lede">A droppable <strong>intelligence</strong> (intelligence/0.2): a signed <code>rwa-agent/1</code> role you can drop onto another rewritable to retune its &#8984;K editor. This file is the carrier — open it, read it, drop it.</p>\n' +
75
+ '<h2>What it does</h2>\n<p>' + esc(prompt) + '</p>\n' +
76
+ '<h2>What it carries</h2>\n<ul>\n<li><strong>Role:</strong> <code>' + esc(role) + '</code></li>' + recLine + affLine + vaultLine + '\n</ul>\n' +
77
+ '<h2>How to use it</h2>\n<p>Drop this file onto another rewritable to install the role (behind the consent dialog), then activate it from the Activity panel&rsquo;s <em>Intelligences</em> section. This carrier is itself a skill-host, so the role is already installed here — try it directly.</p>\n' +
78
+ '</article>';
79
+ }