@sanctuary-framework/mcp-server 1.2.0 → 1.2.2

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.
Files changed (36) hide show
  1. package/dist/cli.cjs +1952 -405
  2. package/dist/cli.cjs.map +1 -1
  3. package/dist/cli.js +1953 -406
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.cjs +1646 -305
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +105 -18
  8. package/dist/index.d.ts +105 -18
  9. package/dist/index.js +1646 -306
  10. package/dist/index.js.map +1 -1
  11. package/dist/templates/coding-assistant/commitments.json +14 -0
  12. package/dist/templates/coding-assistant/defaults.json +34 -0
  13. package/dist/templates/coding-assistant/onboarding.md +24 -0
  14. package/dist/templates/coding-assistant/policy.md +1 -0
  15. package/dist/templates/coding-assistant/template.json +23 -0
  16. package/dist/templates/handoff-coordinator/commitments.json +14 -0
  17. package/dist/templates/handoff-coordinator/defaults.json +10 -0
  18. package/dist/templates/handoff-coordinator/onboarding.md +23 -0
  19. package/dist/templates/handoff-coordinator/policy.md +1 -0
  20. package/dist/templates/handoff-coordinator/template.json +17 -0
  21. package/dist/templates/ops-runner/commitments.json +14 -0
  22. package/dist/templates/ops-runner/defaults.json +12 -0
  23. package/dist/templates/ops-runner/onboarding.md +25 -0
  24. package/dist/templates/ops-runner/policy.md +1 -0
  25. package/dist/templates/ops-runner/template.json +16 -0
  26. package/dist/templates/planner/commitments.json +9 -0
  27. package/dist/templates/planner/defaults.json +10 -0
  28. package/dist/templates/planner/onboarding.md +22 -0
  29. package/dist/templates/planner/policy.md +1 -0
  30. package/dist/templates/planner/template.json +8 -0
  31. package/dist/templates/research-assistant/commitments.json +9 -0
  32. package/dist/templates/research-assistant/defaults.json +25 -0
  33. package/dist/templates/research-assistant/onboarding.md +21 -0
  34. package/dist/templates/research-assistant/policy.md +1 -0
  35. package/dist/templates/research-assistant/template.json +8 -0
  36. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -703,19 +703,40 @@ function assertSanctuaryConfigShape(c) {
703
703
 
704
704
  // src/storage/filesystem.ts
705
705
  init_random();
706
+ var SAFE_CHARS = /[^A-Za-z0-9_.\-]/g;
707
+ function bijectiveEncode(name) {
708
+ return name.replace(
709
+ SAFE_CHARS,
710
+ (ch) => "!" + ch.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase()
711
+ );
712
+ }
713
+ function legacyNamespaceSanitize(name) {
714
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
715
+ }
716
+ function legacyKeySanitize(name) {
717
+ return name.replace(/[^a-zA-Z0-9_.-]/g, "_");
718
+ }
706
719
  var FilesystemStorage = class {
707
720
  basePath;
708
721
  constructor(basePath) {
709
722
  this.basePath = basePath;
710
723
  }
711
724
  entryPath(namespace, key) {
712
- const safeNamespace = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
713
- const safeKey = key.replace(/[^a-zA-Z0-9_.-]/g, "_");
725
+ const safeNamespace = bijectiveEncode(namespace);
726
+ const safeKey = bijectiveEncode(key);
714
727
  return join(this.basePath, safeNamespace, `${safeKey}.enc`);
715
728
  }
716
729
  namespacePath(namespace) {
717
- const safeNamespace = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
718
- return join(this.basePath, safeNamespace);
730
+ return join(this.basePath, bijectiveEncode(namespace));
731
+ }
732
+ // Legacy on-disk paths produced by the pre-#41 sanitizer. Returned for
733
+ // ENOENT-fallback in read/exists/delete; never written to.
734
+ legacyEntryPath(namespace, key) {
735
+ return join(
736
+ this.basePath,
737
+ legacyNamespaceSanitize(namespace),
738
+ `${legacyKeySanitize(key)}.enc`
739
+ );
719
740
  }
720
741
  async write(namespace, key, data) {
721
742
  const dirPath = this.namespacePath(namespace);
@@ -724,7 +745,13 @@ var FilesystemStorage = class {
724
745
  await writeFile(filePath, data, { mode: 384 });
725
746
  }
726
747
  async read(namespace, key) {
727
- const filePath = this.entryPath(namespace, key);
748
+ const buf = await this.readAtPath(this.entryPath(namespace, key));
749
+ if (buf !== null) return buf;
750
+ const legacy = this.legacyEntryPath(namespace, key);
751
+ if (legacy === this.entryPath(namespace, key)) return null;
752
+ return this.readAtPath(legacy);
753
+ }
754
+ async readAtPath(filePath) {
728
755
  try {
729
756
  const buf = await readFile(filePath);
730
757
  return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
@@ -736,7 +763,13 @@ var FilesystemStorage = class {
736
763
  }
737
764
  }
738
765
  async delete(namespace, key, secureOverwrite = true) {
739
- const filePath = this.entryPath(namespace, key);
766
+ const newPath = this.entryPath(namespace, key);
767
+ if (await this.deleteAtPath(newPath, secureOverwrite)) return true;
768
+ const legacy = this.legacyEntryPath(namespace, key);
769
+ if (legacy === newPath) return false;
770
+ return this.deleteAtPath(legacy, secureOverwrite);
771
+ }
772
+ async deleteAtPath(filePath, secureOverwrite) {
740
773
  try {
741
774
  if (secureOverwrite) {
742
775
  const fileStat = await stat(filePath);
@@ -782,12 +815,19 @@ var FilesystemStorage = class {
782
815
  }
783
816
  }
784
817
  async exists(namespace, key) {
785
- const filePath = this.entryPath(namespace, key);
818
+ const newPath = this.entryPath(namespace, key);
786
819
  try {
787
- await stat(filePath);
820
+ await stat(newPath);
788
821
  return true;
789
822
  } catch {
790
- return false;
823
+ const legacy = this.legacyEntryPath(namespace, key);
824
+ if (legacy === newPath) return false;
825
+ try {
826
+ await stat(legacy);
827
+ return true;
828
+ } catch {
829
+ return false;
830
+ }
791
831
  }
792
832
  }
793
833
  async totalSize() {
@@ -4773,6 +4813,32 @@ function canonicalizeForSigning(body) {
4773
4813
  // src/shr/generator.ts
4774
4814
  init_identity();
4775
4815
  init_encoding();
4816
+
4817
+ // src/mesh/constants.ts
4818
+ var PROTOCOL_VERSION = "0.1";
4819
+ var SIGNATURE_SCHEME_V1 = "ed25519-v1";
4820
+ var RESERVED_EVENT_TYPE_PREFIXES = [
4821
+ "EXTENSION_",
4822
+ "cross_fortress_",
4823
+ "multi_master_"
4824
+ ];
4825
+ function isReservedEventType(s) {
4826
+ return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
4827
+ }
4828
+ var RESERVED_EXTENSION_ENVELOPE_KEYS = [
4829
+ "cross_fortress_read_grant",
4830
+ "cross_fortress_read_query",
4831
+ "cross_fortress_read_response",
4832
+ "multi_master_policy_merge",
4833
+ "audit_replication_full_n_way",
4834
+ "auto_promote_canonical_audit",
4835
+ "agent_live_migration"
4836
+ ];
4837
+ function isReservedExtensionKey(k) {
4838
+ return RESERVED_EXTENSION_ENVELOPE_KEYS.includes(k);
4839
+ }
4840
+
4841
+ // src/shr/generator.ts
4776
4842
  var DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
4777
4843
  var DEFAULT_FRESHNESS_WINDOW_DAYS = 30;
4778
4844
  var DEFAULT_LOW_TIER_DOMINANCE_THRESHOLD = 0.6;
@@ -4923,6 +4989,7 @@ function generateSHR(identityId, opts) {
4923
4989
  return {
4924
4990
  body,
4925
4991
  signed_by: identity.public_key,
4992
+ signature_scheme: SIGNATURE_SCHEME_V1,
4926
4993
  signature: toBase64url(signatureBytes)
4927
4994
  };
4928
4995
  }
@@ -7159,14 +7226,14 @@ function generateDashboardHTML(options) {
7159
7226
  // cookie (set by /auth/session and sent automatically by the
7160
7227
  // browser) or as a ?session= query parameter, both of which Stack
7161
7228
  // A's checkAuth honours. Loopback callers also bypass auth via the
7162
- // v0.10.2 _autoAuthLocalhost path, which is the path moltbook
7229
+ // v0.10.2 _autoAuthLocalhost path, which is the path Mini1
7163
7230
  // hits when the dashboard is auto-opened on 127.0.0.1.
7164
7231
  //
7165
7232
  // The endpoint itself is /events \u2014 Stack A's route table mounts it
7166
7233
  // there, and the previous /api/events URL was a 404 in every real
7167
7234
  // boot from v0.10.0 through v0.10.4. The retry loop that result
7168
7235
  // produced is exactly the "status bar flashing blue continuously"
7169
- // moltbook reported on v0.10.4.
7236
+ // Mini1 reported on v0.10.4.
7170
7237
  const eventSource = new EventSource(API_BASE + '/events');
7171
7238
 
7172
7239
  eventSource.addEventListener('init', (e) => {
@@ -9978,10 +10045,8 @@ var CHANNEL_TEMPLATE_IDS = [
9978
10045
  "read-then-report",
9979
10046
  "scheduled-digest",
9980
10047
  "plan-draft-only",
9981
- "fortress-relay",
9982
- "concierge-loop"
10048
+ "fortress-relay"
9983
10049
  ];
9984
- var COUNTERPARTY_WILDCARD = "*";
9985
10050
  var BUDGET_UNITS = ["tokens", "usd"];
9986
10051
 
9987
10052
  // src/templates/registry.ts
@@ -10149,8 +10214,12 @@ function lintOnboarding(_name, content) {
10149
10214
  function resolveTemplatesDir() {
10150
10215
  const thisFile = fileURLToPath(import.meta.url);
10151
10216
  const thisDir = dirname(thisFile);
10152
- if (thisDir.includes("/dist/")) {
10153
- return thisDir.replace("/dist/templates", "/src/templates");
10217
+ if (thisDir.includes("/dist")) {
10218
+ const templatesSubdir = join(thisDir, "templates");
10219
+ const candidateDir = existsSync(join(thisDir, TEMPLATE_NAMES[0])) ? thisDir : existsSync(join(templatesSubdir, TEMPLATE_NAMES[0])) ? templatesSubdir : null;
10220
+ if (candidateDir) return candidateDir;
10221
+ const srcFallback = thisDir.endsWith("/templates") ? thisDir.replace("/dist/templates", "/src/templates") : join(thisDir.replace("/dist", "/src"), "templates");
10222
+ return srcFallback;
10154
10223
  }
10155
10224
  return thisDir;
10156
10225
  }
@@ -10419,23 +10488,6 @@ var fortressRelay = (params) => {
10419
10488
  setRetentionDays(p, 90);
10420
10489
  return p;
10421
10490
  };
10422
- var conciergeLoop = (params) => {
10423
- const p = basePolicy(params);
10424
- p.source_english = "Bidirectional Q&A with the operator. Reads local fortress state; never writes outward.";
10425
- grantOn(p, "memory", {
10426
- counterparty: params.counterparty,
10427
- action: "read",
10428
- scope: { local_fortress_state_only: true, ...params.scope ?? {} }
10429
- });
10430
- grantOn(p, "outputs", {
10431
- counterparty: params.counterparty || COUNTERPARTY_WILDCARD,
10432
- action: "read",
10433
- scope: { operator_chat_only: true, ...params.scope ?? {} }
10434
- });
10435
- p.egress = { allowlist: [] };
10436
- setRetentionDays(p, 14);
10437
- return p;
10438
- };
10439
10491
  function allowedHostsFromScope(scope) {
10440
10492
  const raw = scope?.allowed_hosts;
10441
10493
  if (!Array.isArray(raw)) return [];
@@ -10476,13 +10528,6 @@ var REGISTRY = {
10476
10528
  label: "Fortress relay",
10477
10529
  description: "Routes signed events between peer fortresses. Commits bind only when both sides sign.",
10478
10530
  factory: fortressRelay
10479
- },
10480
- "concierge-loop": {
10481
- id: "concierge-loop",
10482
- severity: "LOW",
10483
- label: "Concierge loop",
10484
- description: "Bidirectional Q&A with the operator. Reads local fortress state; never writes outward.",
10485
- factory: conciergeLoop
10486
10531
  }
10487
10532
  };
10488
10533
  function applyChannelTemplate(id, params) {
@@ -10556,8 +10601,14 @@ function encode(value) {
10556
10601
  }
10557
10602
  function encodeArray(arr) {
10558
10603
  const parts = [];
10559
- for (const item of arr) {
10560
- parts.push(item === void 0 ? "null" : encode(item));
10604
+ for (let i = 0; i < arr.length; i++) {
10605
+ const item = arr[i];
10606
+ if (item === void 0) {
10607
+ throw new MeshCanonicalJsonError(
10608
+ `canonicalize(): undefined is not a valid JSON value at array index ${i}`
10609
+ );
10610
+ }
10611
+ parts.push(encode(item));
10561
10612
  }
10562
10613
  return "[" + parts.join(",") + "]";
10563
10614
  }
@@ -10862,29 +10913,6 @@ function encodePolicyBlob(policy) {
10862
10913
  init_encoding();
10863
10914
  init_random();
10864
10915
 
10865
- // src/mesh/constants.ts
10866
- var PROTOCOL_VERSION = "0.1";
10867
- var RESERVED_EVENT_TYPE_PREFIXES = [
10868
- "EXTENSION_",
10869
- "cross_fortress_",
10870
- "multi_master_"
10871
- ];
10872
- function isReservedEventType(s) {
10873
- return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
10874
- }
10875
- var RESERVED_EXTENSION_ENVELOPE_KEYS = [
10876
- "cross_fortress_read_grant",
10877
- "cross_fortress_read_query",
10878
- "cross_fortress_read_response",
10879
- "multi_master_policy_merge",
10880
- "audit_replication_full_n_way",
10881
- "auto_promote_canonical_audit",
10882
- "agent_live_migration"
10883
- ];
10884
- function isReservedExtensionKey(k) {
10885
- return RESERVED_EXTENSION_ENVELOPE_KEYS.includes(k);
10886
- }
10887
-
10888
10916
  // src/mesh/trust-root.ts
10889
10917
  init_encoding();
10890
10918
  init_identity();
@@ -12105,7 +12133,7 @@ async function api(path, opts) {
12105
12133
  // /policies, /activity responses on subsequent GETs even when the
12106
12134
  // server-side state has changed (e.g. recent-failures buffer cleared
12107
12135
  // on substrate flip). The pre-rc.5 client used bare fetch with no
12108
- // cache control, which on moltbook Safari produced a stale view of
12136
+ // cache control, which on Mini1 Safari produced a stale view of
12109
12137
  // server state and made the operator-visible badge color stick to
12110
12138
  // its prior value across substrate changes. Belt + suspenders:
12111
12139
  // cache: "no-store" turns off the response cache; the _t query
@@ -12267,12 +12295,6 @@ const CHANNEL_TEMPLATES = [
12267
12295
  severity: "MEDIUM",
12268
12296
  title: "Fortress relay",
12269
12297
  description: "Routes signed events between peer fortresses. Commits bind only when both sides sign."
12270
- },
12271
- {
12272
- id: "concierge-loop",
12273
- severity: "LOW",
12274
- title: "Concierge loop",
12275
- description: "Bidirectional Q&A with the operator. Reads local fortress state; never writes outward."
12276
12298
  }
12277
12299
  ];
12278
12300
 
@@ -12319,6 +12341,24 @@ function setRoute(route) {
12319
12341
  renderFortress();
12320
12342
  }
12321
12343
 
12344
+ // Renders the global attestation badge (Q1 layer 1, persistent across
12345
+ // surfaces). Tone is driven by state.topbarPills.attestation. Pending
12346
+ // state shows a dashed seal ring; verified shows solid; degraded shows
12347
+ // outlined core; unverified shows the broken-seal mark. Observation
12348
+ // language only; Castle Layer 1 enforcement ships in WP-V1.x-CASTLE-WALL.
12349
+ function renderTopbarAttestationBadge(stateName) {
12350
+ const valid = stateName === "verified" || stateName === "degraded" || stateName === "unverified" || stateName === "pending";
12351
+ const cls = valid ? stateName : "pending";
12352
+ const ringDashed = cls === "pending" ? " dashed" : "";
12353
+ return '<span class="att-global ' + cls + '" data-pill="attestation" title="Fortress attestation">' +
12354
+ '<span class="seal">' +
12355
+ '<span class="seal-ring' + ringDashed + '"></span>' +
12356
+ '<span class="seal-core"></span>' +
12357
+ '</span>' +
12358
+ '<span class="label">' + escHtml(cls) + '</span>' +
12359
+ '</span>';
12360
+ }
12361
+
12322
12362
  function renderTopbar() {
12323
12363
  const pillEl = document.getElementById("topbar-pills");
12324
12364
  if (!pillEl) return;
@@ -12335,7 +12375,7 @@ function renderTopbar() {
12335
12375
  versionPill,
12336
12376
  '<span class="pill" data-pill="deployment">deployment: ' + escHtml(state.topbarPills.deployment) + '</span>',
12337
12377
  '<span class="pill" data-pill="mode">mode: ' + escHtml(state.topbarPills.mode) + '</span>',
12338
- '<span class="pill tone-' + escHtml(state.topbarPills.attestation) + '" data-pill="attestation">attestation: ' + escHtml(state.topbarPills.attestation) + '</span>'
12378
+ renderTopbarAttestationBadge(state.topbarPills.attestation)
12339
12379
  ].join("");
12340
12380
  // Lockdown button three-state UX (binding addendum 3).
12341
12381
  const btn = document.getElementById("btn-lockdown");
@@ -12431,6 +12471,7 @@ function renderMain() {
12431
12471
  case "agent-detail": nextHtml = renderAgentDetail(); break;
12432
12472
  case "policy": nextHtml = renderPolicyCenter(); break;
12433
12473
  case "intelligence": nextHtml = renderIntelligenceCenter(); break;
12474
+ case "attestation": nextHtml = renderAttestation(); break;
12434
12475
  case "privacy": nextHtml = renderPrivacyPage(); break;
12435
12476
  case "coordination": nextHtml = renderCoordinationPage(); break;
12436
12477
  case "health": nextHtml = renderHealthPage(); break;
@@ -12509,9 +12550,9 @@ function renderMain() {
12509
12550
  // "Concierge unavailable; substrate not configured") sourced from the
12510
12551
  // last response's served_by + display_label.
12511
12552
  const CONCIERGE_SUGGESTIONS = [
12512
- { id: "summarize-hour", label: "summarize the last hour", query: "Summarize what happened in this fortress in the last hour." },
12513
- { id: "agent-touched", label: "what has each agent touched today", query: "What has each wrapped agent done today? Group by agent." },
12514
- { id: "open-approvals", label: "any open approvals?", query: "Are there any open Tier 1 approvals or pending inbox items I should look at?" }
12553
+ { id: "summarize-hour", category: "Summarize", label: "summarize the last hour", query: "Summarize what happened in this fortress in the last hour." },
12554
+ { id: "agent-touched", category: "Inspect", label: "what has each agent touched today", query: "What has each wrapped agent done today? Group by agent." },
12555
+ { id: "open-approvals", category: "Approvals", label: "any open approvals?", query: "Are there any open Tier 1 approvals or pending inbox items I should look at?" }
12515
12556
  ];
12516
12557
 
12517
12558
  // Direct-agent chat surface was removed in the v1.2 reshape; the
@@ -12528,59 +12569,316 @@ function renderDashboardConcierge() {
12528
12569
  const badge = c.badge && c.badge.displayLabel
12529
12570
  ? '<span class="pill mono concierge-badge" title="Substrate that served the most recent response">' + escHtml(c.badge.displayLabel) + '</span>'
12530
12571
  : '<span class="pill muted concierge-badge">Concierge: substrate not yet contacted</span>';
12531
- const messages = c.messages.length
12572
+ const sendDisabled = c.sending ? ' disabled' : '';
12573
+ const sendLabel = c.sending ? 'Sending...' : 'Send';
12574
+ // Sprint Piece 2 PR 2: empty state lives INSIDE the concierge-history
12575
+ // container so the DDD e2e selector .concierge-history matches both
12576
+ // empty and active state. The container's flex layout hosts a single
12577
+ // .concierge-empty child that fills the available height with a serif
12578
+ // headline and a 3-up suggest grid; the grid replaces the v1.2 bottom
12579
+ // chip row, which is retired with this polish.
12580
+ const emptyState =
12581
+ '<div class="concierge-empty">' +
12582
+ '<div class="concierge-empty-headline">' +
12583
+ '<h2>Where would you like to begin.</h2>' +
12584
+ '<p>Ask anything about your fortress. Sanctuary holds your context, your agents, your policy. It will answer plainly, or hand you to the right surface.</p>' +
12585
+ '</div>' +
12586
+ '<div class="concierge-suggest-grid">' +
12587
+ CONCIERGE_SUGGESTIONS.map(function (s) {
12588
+ return '<button class="concierge-suggest" data-action="concierge-suggestion" data-suggestion-id="' + escHtml(s.id) + '"' + sendDisabled + '>' +
12589
+ '<span class="label">' + escHtml(s.category || '') + '</span>' +
12590
+ escHtml(s.label) +
12591
+ '</button>';
12592
+ }).join("") +
12593
+ '</div>' +
12594
+ '</div>';
12595
+ const messagesHtml = c.messages.length
12532
12596
  ? c.messages.map(function (m) {
12533
12597
  const cls = m.role === "operator" ? "concierge-msg-operator" : "concierge-msg-concierge";
12534
- const author = m.role === "operator" ? "you" : "Sanctuary Fortress concierge";
12598
+ const authorLabel = m.role === "operator" ? "you" : "sanctuary";
12599
+ const metaParts = [];
12600
+ if (m.created_at) metaParts.push(escHtml(shortTime(m.created_at)));
12601
+ if (m.role === "concierge" && m.served_by) metaParts.push('substrate: ' + escHtml(m.served_by));
12602
+ const meta = metaParts.length
12603
+ ? '<div class="concierge-msg-meta"><span>' + metaParts.join(' · ') + '</span></div>'
12604
+ : '';
12535
12605
  return '<div class="concierge-msg ' + cls + '">' +
12536
- '<div class="concierge-msg-author muted">' + escHtml(author) + ' · ' + escHtml(shortTime(m.created_at)) + '</div>' +
12606
+ '<span class="concierge-msg-author">' + escHtml(authorLabel) + '</span>' +
12537
12607
  '<div class="concierge-msg-body">' + escHtml(m.body) + '</div>' +
12608
+ meta +
12538
12609
  '</div>';
12539
12610
  }).join("\n")
12540
- : '<p class="muted concierge-empty">No messages yet. Ask the concierge anything about your fortress: it can summarize agent activity, surface open approvals, or describe the current policy.</p>';
12611
+ : emptyState;
12541
12612
  const errorBanner = c.error
12542
12613
  ? '<div class="banner banner-warn">' + escHtml(c.error) + '</div>'
12543
12614
  : "";
12544
- const sendDisabled = c.sending ? ' disabled' : '';
12545
- const sendLabel = c.sending ? 'Sending...' : 'Send';
12546
- const chips = CONCIERGE_SUGGESTIONS.map(function (s) {
12547
- return '<button class="btn chip" data-action="concierge-suggestion" data-suggestion-id="' + escHtml(s.id) + '"' + sendDisabled + '>' + escHtml(s.label) + '</button>';
12548
- }).join("\n");
12549
12615
  const activeChatsPanel = renderActiveChatsPanel();
12550
12616
  return [
12551
- '<h1>Chat <span class="muted">/ This fortress</span></h1>',
12552
- activeChatsPanel,
12553
- '<div class="card concierge-card">',
12554
- '<div class="concierge-header">',
12555
- '<div class="concierge-persona"><strong>Sanctuary Fortress concierge</strong> <span class="muted">read-only over fortress state</span></div>',
12556
- badge,
12617
+ '<div class="concierge-wrap">',
12618
+ '<div class="page-head"><div>',
12619
+ '<p class="eyebrow">Concierge</p>',
12620
+ '<h1>Talk to your fortress.</h1>',
12621
+ '<p class="sub">A direct line to Sanctuary, routed through the substrate you chose. Nothing leaves without your hand on it.</p>',
12622
+ '</div></div>',
12623
+ activeChatsPanel,
12624
+ '<div class="card concierge-card">',
12625
+ '<div class="concierge-header">',
12626
+ '<div class="concierge-persona">',
12627
+ '<div class="glyph-ring"></div>',
12628
+ '<div class="concierge-persona-text"><strong>Sanctuary Fortress concierge</strong><small>read-only over fortress state</small></div>',
12629
+ '</div>',
12630
+ '<div class="concierge-meta">' + badge + '</div>',
12631
+ '</div>',
12632
+ errorBanner,
12633
+ '<div class="concierge-history" id="concierge-history">' + messagesHtml + '</div>',
12634
+ '<form class="concierge-composer" data-action="concierge-submit">',
12635
+ '<div class="input-wrap">',
12636
+ '<input type="text" name="concierge-input" placeholder="Type to Sanctuary. Enter to send." value="' + escHtml(c.composer) + '" data-action="concierge-input"' + sendDisabled + ' autocomplete="off">',
12637
+ '<span class="composer-meta">Enter</span>',
12638
+ '</div>',
12639
+ '<button type="submit" class="btn btn-primary" data-action="concierge-send"' + sendDisabled + '>' + escHtml(sendLabel) + '</button>',
12640
+ '</form>',
12641
+ '<p class="muted concierge-foot">First time? <a href="#intelligence">Pick a substrate</a> to enable concierge replies.</p>',
12557
12642
  '</div>',
12558
- errorBanner,
12559
- '<div class="concierge-history" id="concierge-history">' + messages + '</div>',
12560
- '<form class="concierge-composer" data-action="concierge-submit">',
12561
- '<input type="text" name="concierge-input" placeholder="Ask the concierge about this fortress..." value="' + escHtml(c.composer) + '" data-action="concierge-input"' + sendDisabled + ' autocomplete="off">',
12562
- '<button type="submit" class="btn btn-primary" data-action="concierge-send"' + sendDisabled + '>' + escHtml(sendLabel) + '</button>',
12563
- '</form>',
12564
- '<div class="concierge-chips">' + chips + '</div>',
12565
- '<p class="muted concierge-foot">First time? <a href="#intelligence">Pick a substrate</a> to enable concierge replies.</p>',
12566
12643
  '</div>'
12567
12644
  ].join("");
12568
12645
  }
12569
12646
 
12570
12647
  // ── Render: agents list / detail ───────────────────────────────────────
12648
+ //
12649
+ // Sprint Piece 2 PR 4 polish: empty state uses .agents-empty with the
12650
+ // concentric icon-frame + a terminal-block CTA. Populated state uses the
12651
+ // .agents-layout grid with the .agents-list 4-column table (Agent /
12652
+ // State / Attestation / Last seen). The empty-state branch keeps the
12653
+ // literal '<h1>Agents</h1>' start and the "No wrapped agents yet." copy
12654
+ // because agents-empty-state-canary.test.ts pins both.
12655
+ function agentInitials(agentId) {
12656
+ const tail = String(agentId || "").split(":").pop() || "";
12657
+ const cleaned = tail.replace(/[^a-zA-Z0-9]/g, "");
12658
+ return (cleaned.slice(0, 2) || "??").toUpperCase();
12659
+ }
12660
+ function agentStateClass(status) {
12661
+ if (status === "active") return "live";
12662
+ if (status === "locked_down" || status === "error") return "off";
12663
+ return "idle";
12664
+ }
12665
+ // Per-agent attestation badge (Q1 layer 2). Square chip beside each
12666
+ // agent: a bounded glyph beside a bounded entity. Color and fill pattern
12667
+ // carry meaning together so the badge reads even monochrome. The "locked"
12668
+ // status maps to the unverified visual (rust + hatched mark) since a
12669
+ // locked-down agent has no current attestation; the inspect-pane copy
12670
+ // explains the distinction. Pure visual surface; no state derivation.
12671
+ function renderAgentAttestationBadge(status) {
12672
+ let cls;
12673
+ let label;
12674
+ if (status === "active") { cls = "verified"; label = "verified"; }
12675
+ else if (status === "locked_down") { cls = "unverified"; label = "locked"; }
12676
+ else if (status === "error") { cls = "unverified"; label = "unverified"; }
12677
+ else { cls = "degraded"; label = "degraded"; }
12678
+ return '<span class="att-agent ' + cls + '" title="Agent attestation"><span class="mark"></span>' + escHtml(label) + '</span>';
12679
+ }
12680
+ // Per-action attestation tick (Q1 layer 3). Tiny inline shape on every
12681
+ // timeline row. Two-byte signature fragment is enough at low resolution;
12682
+ // the full signature is one click away. Neutral state shows a circle
12683
+ // instead of a tick when the signer was unreachable; the action is still
12684
+ // recorded. Visual surface only.
12685
+ function renderActionAttestationBadge(stateName, sig) {
12686
+ const valid = stateName === "verified" || stateName === "degraded" || stateName === "unverified" || stateName === "neutral";
12687
+ const cls = valid ? stateName : "neutral";
12688
+ const sigText = sig ? String(sig) : "--";
12689
+ return '<span class="att-action ' + cls + '" title="Action attestation">' +
12690
+ '<span class="tick"></span>' +
12691
+ '<span>' + escHtml(sigText) + '</span>' +
12692
+ '</span>';
12693
+ }
12694
+ // Attestation gallery surface (Q1 four classes: global / per-agent /
12695
+ // per-action / per-transaction custody-provenance stub). Reference for
12696
+ // operators: shows what each badge looks like across verified, degraded,
12697
+ // unverified, and (where applicable) pending or neutral states. Pure
12698
+ // visual; no derivation, no live data. Castle Layer 3 cooperative-MCP UX
12699
+ // surface; Castle Layer 1 enforcement ships in WP-V1.x-CASTLE-WALL.
12700
+ function renderAttestation() {
12701
+ return '<div class="att-gallery">' +
12702
+ '<div class="page-head"><div>' +
12703
+ '<p class="eyebrow">Attestation</p>' +
12704
+ '<h1>Four classes of badge.</h1>' +
12705
+ '<p class="sub">A signature you can see. From the whole fortress, down to a single action. Degrade, never destroy: a failed signature becomes neutral with a tooltip; the surface keeps working.</p>' +
12706
+ '</div></div>' +
12707
+ // Global
12708
+ '<div class="att-section">' +
12709
+ '<div class="att-section-head"><div>' +
12710
+ '<h2>Global. The fortress itself.</h2>' +
12711
+ '<p>Lives in the topbar. Visible on every surface. Tells you the fortress identity is currently signed and matches the binary you installed.</p>' +
12712
+ '</div><span class="label">topbar</span></div>' +
12713
+ attRow(renderTopbarAttestationBadge("verified"), "Verified", "Identity matches. Binary matches. Default state for a healthy fortress.") +
12714
+ attRow(renderTopbarAttestationBadge("degraded"), "Degraded", "The signature is older than the staleness window, or one of two co-signers is unreachable. The fortress keeps running.") +
12715
+ attRow(renderTopbarAttestationBadge("unverified"), "Unverified", "The signature did not validate. The surface still works; lockdown is still available; the badge tells you to investigate.") +
12716
+ attRow(renderTopbarAttestationBadge("pending"), "Pending", "First-run state. Fortress is signing for the first time. Settles in seconds.") +
12717
+ '</div>' +
12718
+ // Per-agent
12719
+ '<div class="att-section">' +
12720
+ '<div class="att-section-head"><div>' +
12721
+ '<h2>Per-agent. In the agents list and inspect pane.</h2>' +
12722
+ '<p>A square chip beside each agent. Square because an agent is bounded; the fortress (a circle) contains it.</p>' +
12723
+ '</div><span class="label">agents view</span></div>' +
12724
+ '<div class="att-row">' +
12725
+ '<div class="demo" style="display:flex; gap:8px; flex-wrap:wrap;">' +
12726
+ renderAgentAttestationBadge("active") +
12727
+ renderAgentAttestationBadgeForState("degraded", "degraded") +
12728
+ renderAgentAttestationBadgeForState("unverified", "unverified") +
12729
+ '</div>' +
12730
+ '<div class="desc"><strong>Verified, degraded, unverified</strong>' +
12731
+ '<small>Color and the fill pattern carry meaning together. A solid square reads "attested" at a glance; a hatched square reads "trouble" at a glance, even monochrome.</small>' +
12732
+ '</div>' +
12733
+ '</div>' +
12734
+ '</div>' +
12735
+ // Per-action
12736
+ '<div class="att-section">' +
12737
+ '<div class="att-section-head"><div>' +
12738
+ '<h2>Per-action. Inline in the activity timeline.</h2>' +
12739
+ '<p>Each entry in any timeline carries a small signature fragment. Hover to expand. A tick instead of a fill keeps the row visually quiet at low resolution.</p>' +
12740
+ '</div><span class="label">timeline</span></div>' +
12741
+ '<div class="att-row">' +
12742
+ '<div class="demo" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">' +
12743
+ '<span style="font-size:13px; color:var(--ink-2);">14:22:08 doc-reviewer summarized intake.pdf</span>' +
12744
+ renderActionAttestationBadge("verified", "9c7d..2a") +
12745
+ '</div>' +
12746
+ '<div class="desc"><strong>Verified action</strong>' +
12747
+ '<small>The most common shape. Two-byte signature fragment is enough; the full signature is one click away.</small>' +
12748
+ '</div>' +
12749
+ '</div>' +
12750
+ '<div class="att-row">' +
12751
+ '<div class="demo" style="display:flex; gap:10px; align-items:center;">' +
12752
+ '<span style="font-size:13px; color:var(--ink-2);">14:11:47 privacy filter redacted payload</span>' +
12753
+ renderActionAttestationBadge("degraded", "b440..71") +
12754
+ '</div>' +
12755
+ '<div class="desc"><strong>Degraded action</strong>' +
12756
+ '<small>The action signed, but the signature class was less than the policy preferred. Useful when a substrate is still warming up.</small>' +
12757
+ '</div>' +
12758
+ '</div>' +
12759
+ '<div class="att-row">' +
12760
+ '<div class="demo" style="display:flex; gap:10px; align-items:center;">' +
12761
+ '<span style="font-size:13px; color:var(--ink-2);">14:09:02 agent attempted external link</span>' +
12762
+ renderActionAttestationBadge("neutral", "--") +
12763
+ '</div>' +
12764
+ '<div class="desc"><strong>Neutral. Degrade, not destroy.</strong>' +
12765
+ '<small>The signer was unreachable. Rather than hide the action, the badge becomes neutral and a tooltip explains. The action is still recorded.</small>' +
12766
+ '</div>' +
12767
+ '</div>' +
12768
+ '</div>' +
12769
+ // Custody stub
12770
+ '<div class="att-section">' +
12771
+ '<div class="att-section-head"><div>' +
12772
+ '<h2>Custody. Stub for v1.x.</h2>' +
12773
+ '<p>A fourth class, surfaced conservatively. Reserved for forthcoming custody-provenance signatures (x402 payment receipts, ERC-8004 identity assertions). Visible, dashed, clearly stubbed.</p>' +
12774
+ '</div><span class="label">stub</span></div>' +
12775
+ '<div class="att-row">' +
12776
+ '<div class="demo">' +
12777
+ '<span class="att-custody" title="Custody-provenance, v1.x">' +
12778
+ '<span class="seal-stub"></span>' +
12779
+ '<span class="stub-tag">custody. stub</span>' +
12780
+ '</span>' +
12781
+ '</div>' +
12782
+ '<div class="desc"><strong>Custody. Stub.</strong>' +
12783
+ '<small>Dashed border signals "shape reserved, content pending." Will populate when custody signatures land in a future release. Cannot be confused with a verified badge at any zoom level.</small>' +
12784
+ '</div>' +
12785
+ '</div>' +
12786
+ '</div>' +
12787
+ // Tooltip
12788
+ '<div class="att-section">' +
12789
+ '<div class="att-section-head"><div>' +
12790
+ '<h2>Tooltip on failure.</h2>' +
12791
+ '<p>A failed badge is never silent. The tooltip explains in plain language, suggests one action, and confirms the surface is still working.</p>' +
12792
+ '</div><span class="label">degrade not destroy</span></div>' +
12793
+ '<div class="att-row">' +
12794
+ '<div class="demo">' +
12795
+ '<span class="att-tooltip">The signer at sig.fortress.local did not respond in 4s. Your fortress kept working. Try: open Health to see the signer status.</span>' +
12796
+ '</div>' +
12797
+ '<div class="desc"><strong>Plain-language tooltip</strong>' +
12798
+ '<small>Three lines, in order: what happened, what did not break, what to do. No jargon, no stack trace.</small>' +
12799
+ '</div>' +
12800
+ '</div>' +
12801
+ '</div>' +
12802
+ '</div>';
12803
+ }
12804
+ function attRow(demoHtml, strong, smallText) {
12805
+ return '<div class="att-row">' +
12806
+ '<div class="demo">' + demoHtml + '</div>' +
12807
+ '<div class="desc"><strong>' + escHtml(strong) + '</strong>' +
12808
+ '<small>' + escHtml(smallText) + '</small>' +
12809
+ '</div>' +
12810
+ '</div>';
12811
+ }
12812
+ // Gallery-only variant: render a per-agent badge for a given visual state
12813
+ // (verified / degraded / unverified) without going through the agent
12814
+ // status mapping. Used by renderAttestation to show all three states
12815
+ // side by side as design reference.
12816
+ function renderAgentAttestationBadgeForState(cls, label) {
12817
+ return '<span class="att-agent ' + escHtml(cls) + '" title="Agent attestation"><span class="mark"></span>' + escHtml(label) + '</span>';
12818
+ }
12819
+ function relTimeFromIso(iso) {
12820
+ if (!iso) return "";
12821
+ const d = new Date(iso);
12822
+ if (isNaN(d.getTime())) return iso;
12823
+ const diffMs = Date.now() - d.getTime();
12824
+ const diffSec = Math.max(0, Math.floor(diffMs / 1000));
12825
+ if (diffSec < 60) return diffSec + "s ago";
12826
+ const diffMin = Math.floor(diffSec / 60);
12827
+ if (diffMin < 60) return diffMin + "m ago";
12828
+ const diffHr = Math.floor(diffMin / 60);
12829
+ if (diffHr < 24) return diffHr + "h ago";
12830
+ const diffDay = Math.floor(diffHr / 24);
12831
+ return diffDay + "d ago";
12832
+ }
12571
12833
  function renderAgentsList() {
12572
- if (!state.agents.length) return '<h1>Agents</h1><p class="muted">No wrapped agents yet. Run <code>sanctuary wrap</code> to wrap a harness.</p>';
12834
+ if (!state.agents.length) return '<h1>Agents</h1>' +
12835
+ '<div class="agents-empty">' +
12836
+ '<div class="icon-frame"><div class="core"></div></div>' +
12837
+ '<h2>No wrapped agents yet.</h2>' +
12838
+ '<p>Wrap an agent to give it a portable identity, a charter, and approval gates. Run <code>sanctuary wrap</code> in any project where your agent lives.</p>' +
12839
+ '<div class="terminal-block"><span class="cmd"><span class="prompt">$</span>sanctuary wrap</span></div>' +
12840
+ '</div>';
12841
+ const count = state.agents.length;
12842
+ const subCopy = count + ' wrapped. Click one to inspect its activity, policy, and pending approvals.';
12573
12843
  const rows = state.agents.map(function (a) {
12574
12844
  const map = STATUS_MAP[a.status] || STATUS_MAP.unknown;
12575
- const reason = a.status_reason_class ? (REASON_LABELS[a.status_reason_class] || "") : "";
12576
- return '<div class="row">' +
12577
- '<span class="glyph ' + map.glyph + '"></span>' +
12578
- '<div class="grow"><strong>' + escHtml(a.agent_id) + '</strong> <span class="muted mono">' + escHtml(a.harness) + '</span></div>' +
12579
- '<span class="pill" title="' + escHtml(reason) + '">' + escHtml(map.label) + '</span>' +
12580
- '<button class="btn" data-action="open-agent" data-agent-id="' + escHtml(a.agent_id) + '">Open</button>' +
12845
+ const dotCls = agentStateClass(a.status);
12846
+ const initials = agentInitials(a.agent_id);
12847
+ const role = escHtml(a.harness) + (a.model_provider && a.model_provider.model_id ? ' · ' + escHtml(a.model_provider.model_id) : '');
12848
+ const isSelected = state.selectedAgentId === a.agent_id;
12849
+ return '<div class="agent-row' + (isSelected ? ' selected' : '') + '" data-action="open-agent" data-agent-id="' + escHtml(a.agent_id) + '" role="button" tabindex="0" title="Open inspect panel for ' + escHtml(a.agent_id) + '">' +
12850
+ '<div class="agent-identity">' +
12851
+ '<div class="agent-glyph">' + escHtml(initials) + '</div>' +
12852
+ '<div class="agent-name">' +
12853
+ '<strong>' + escHtml(a.agent_id) + '</strong>' +
12854
+ '<small>' + role + '</small>' +
12855
+ '</div>' +
12856
+ '</div>' +
12857
+ '<span class="agent-state">' +
12858
+ '<span class="state-dot ' + dotCls + '"></span>' +
12859
+ escHtml(map.label) +
12860
+ '</span>' +
12861
+ renderAgentAttestationBadge(a.status) +
12862
+ '<span class="agent-last">' + escHtml(relTimeFromIso(a.last_activity_at)) + '</span>' +
12581
12863
  '</div>';
12582
12864
  }).join("\n");
12583
- return '<h1>Agents</h1><div class="card">' + rows + '</div>';
12865
+ return '<div class="agents-wrap">' +
12866
+ '<div class="page-head">' +
12867
+ '<div>' +
12868
+ '<p class="eyebrow">Agents</p>' +
12869
+ '<h1>Agents.</h1>' +
12870
+ '<p class="sub">' + escHtml(subCopy) + '</p>' +
12871
+ '</div>' +
12872
+ '</div>' +
12873
+ '<div class="agents-layout">' +
12874
+ '<div class="agents-list">' +
12875
+ '<div class="agents-list-head">' +
12876
+ '<span>Agent</span><span>State</span><span>Attestation</span><span>Last seen</span>' +
12877
+ '</div>' +
12878
+ rows +
12879
+ '</div>' +
12880
+ '</div>' +
12881
+ '</div>';
12584
12882
  }
12585
12883
 
12586
12884
  function renderAgentDetail() {
@@ -12591,7 +12889,10 @@ function renderAgentDetail() {
12591
12889
  const timeline = events.length
12592
12890
  ? events.map(function (e) {
12593
12891
  const t = renderTemplate(e.display_template_id, e.display_template_args);
12594
- return '<div class="row"><span class="muted">' + escHtml(shortTime(e.emitted_at)) + '</span><span>' + escHtml(t) + '</span></div>';
12892
+ const badgeHtml = e.attestation
12893
+ ? ' ' + renderActionAttestationBadge(e.attestation.state, e.attestation.fragment)
12894
+ : '';
12895
+ return '<div class="row"><span class="muted">' + escHtml(shortTime(e.emitted_at)) + '</span><span>' + escHtml(t) + badgeHtml + '</span></div>';
12595
12896
  }).join("\n")
12596
12897
  : '<p class="muted">No activity yet.</p>';
12597
12898
  // WP-V1.2 reshape click-to-inspect surface. Clicking "Open inspect
@@ -12635,53 +12936,99 @@ function renderAgentInspectPanel(agent) {
12635
12936
  : "";
12636
12937
 
12637
12938
  // State 2: panel loaded.
12939
+ // Sprint Piece 2 PR 4 polish: outer wrapper combines .card with
12940
+ // .inspect-pane (sticky right rail, internal scroll, sectioned body).
12941
+ // The .card class is preserved so the rendered surface keeps its
12942
+ // shared card chrome; .inspect-pane overrides .card padding so the
12943
+ // inspect-head and inspect-body control their own spacing per design.
12638
12944
  if (panel) {
12945
+ const dotCls = agentStateClass(agent.status);
12946
+ const stateMap = STATUS_MAP[agent.status] || STATUS_MAP.unknown;
12639
12947
  const activity = (panel.recent_activity || []).slice(0, 20);
12640
12948
  const activityHtml = activity.length
12641
- ? activity.map(function (e) {
12949
+ ? '<div class="timeline">' +
12950
+ activity.map(function (e) {
12642
12951
  const t = renderTemplate(e.display_template_id, e.display_template_args);
12643
- return '<div class="row"><span class="muted mono">' + escHtml(shortTime(e.emitted_at)) + '</span><span>' + escHtml(t) + '</span></div>';
12644
- }).join("\n")
12952
+ const badgeHtml = e.attestation
12953
+ ? renderActionAttestationBadge(e.attestation.state, e.attestation.fragment)
12954
+ : '';
12955
+ return '<div class="timeline-item ok">' +
12956
+ '<div class="ts">' + escHtml(shortTime(e.emitted_at)) + '</div>' +
12957
+ '<div class="what">' + escHtml(t) + '</div>' +
12958
+ (badgeHtml ? '<div class="att">' + badgeHtml + '</div>' : '') +
12959
+ '</div>';
12960
+ }).join("") +
12961
+ '</div>'
12645
12962
  : '<p class="muted">No recent activity for this agent.</p>';
12646
12963
 
12647
12964
  const approvals = panel.pending_approvals || [];
12648
12965
  const approvalsHtml = approvals.length
12649
12966
  ? approvals.map(function (item) {
12650
12967
  const promptText = renderTemplate(item.display_template_id, item.display_template_args);
12651
- return '<div class="row">' +
12652
- '<span class="pill tone-info">' + escHtml(item.tier || "tier1") + '</span>' +
12653
- '<div class="grow">' + escHtml(promptText) + '</div>' +
12654
- '<button class="btn btn-primary" data-action="inbox-approve" data-item-id="' + escHtml(item.item_id) + '">Approve</button>' +
12655
- '<button class="btn" data-action="inbox-deny" data-item-id="' + escHtml(item.item_id) + '">Deny</button>' +
12968
+ return '<div class="approval-row">' +
12969
+ '<div class="what">' +
12970
+ '<span class="pill tone-degraded">' + escHtml(item.tier || "tier1") + '</span>' +
12971
+ escHtml(promptText) +
12972
+ '</div>' +
12973
+ '<div class="actions">' +
12974
+ '<button class="btn" data-action="inbox-deny" data-item-id="' + escHtml(item.item_id) + '">Deny</button>' +
12975
+ '<button class="btn btn-primary" data-action="inbox-approve" data-item-id="' + escHtml(item.item_id) + '">Approve once</button>' +
12976
+ '</div>' +
12656
12977
  '</div>';
12657
- }).join("\n")
12978
+ }).join("")
12658
12979
  : '<p class="muted">No pending approvals routed through this agent.</p>';
12659
12980
 
12660
- const policyLine = panel.policy_summary
12661
- ? '<dt>Policy</dt><dd class="mono">' + escHtml(panel.policy_summary.display_label || panel.policy_summary.policy_id) + '</dd>' +
12981
+ const policySection = panel.policy_summary
12982
+ ? '<div class="policy-line"><span class="k">Policy</span><span class="v">' + escHtml(panel.policy_summary.display_label || panel.policy_summary.policy_id) + '</span></div>' +
12662
12983
  (panel.policy_summary.channel_template_id
12663
- ? '<dt>Template</dt><dd class="mono">' + escHtml(panel.policy_summary.channel_template_id) + '</dd>'
12984
+ ? '<div class="policy-line"><span class="k">Template</span><span class="v">' + escHtml(panel.policy_summary.channel_template_id) + '</span></div>'
12664
12985
  : '') +
12665
- '<dt>Bound</dt><dd class="mono">' + escHtml(shortTime(panel.policy_summary.bound_at)) + '</dd>'
12666
- : '<dt>Policy</dt><dd class="muted">No bound policy yet.</dd>';
12667
-
12668
- return '<div class="card">' +
12669
- '<div class="concierge-header">' +
12670
- '<div class="concierge-persona"><strong>Inspect ' + escHtml(agent.agent_id) + '</strong> ' +
12671
- '<span class="muted">opened ' + escHtml(shortTime(panel.opened_at)) + '</span></div>' +
12672
- '<button class="btn" data-action="agent-inspect-open" data-agent-id="' + escHtml(agent.agent_id) + '">Refresh</button>' +
12986
+ '<div class="policy-line"><span class="k">Bound</span><span class="v">' + escHtml(shortTime(panel.policy_summary.bound_at)) + '</span></div>'
12987
+ : '<div class="policy-line"><span class="k">Policy</span><span class="v">No bound policy yet.</span></div>';
12988
+
12989
+ const modelLine = agent.model_provider
12990
+ ? '<div class="policy-line"><span class="k">Model</span><span class="v">' + escHtml(agent.model_provider.vendor) + ' / ' + escHtml(agent.model_provider.model_id) + '</span></div>'
12991
+ : '';
12992
+
12993
+ return '<div class="card inspect-pane">' +
12994
+ '<div class="inspect-head">' +
12995
+ '<div class="row1">' +
12996
+ '<div class="agent-glyph">' + escHtml(agentInitials(agent.agent_id)) + '</div>' +
12997
+ '<h3>' + escHtml(agent.agent_id) + '</h3>' +
12998
+ '<span style="margin-left:auto;">' + renderAgentAttestationBadge(agent.status) + '</span>' +
12999
+ '</div>' +
13000
+ '<div class="meta">' +
13001
+ '<span class="pill ' + (dotCls === "live" ? "tone-verified" : "tone-degraded") + '"><span class="state-dot ' + dotCls + '" style="margin-right:4px;"></span>' + escHtml(stateMap.label) + '</span>' +
13002
+ '<span class="pill">opened ' + escHtml(shortTime(panel.opened_at)) + '</span>' +
13003
+ '<button class="btn btn-quiet" data-action="agent-inspect-open" data-agent-id="' + escHtml(agent.agent_id) + '" title="Refresh inspect panel">Refresh</button>' +
13004
+ '</div>' +
13005
+ '</div>' +
13006
+ '<div class="inspect-body">' +
13007
+ errorBanner +
13008
+ '<div class="inspect-section">' +
13009
+ '<h4>Pending approvals' + (approvals.length ? ' <span class="count">' + approvals.length + '</span>' : '') + '</h4>' +
13010
+ approvalsHtml +
13011
+ '</div>' +
13012
+ '<div class="inspect-section">' +
13013
+ '<h4>Recent activity</h4>' +
13014
+ activityHtml +
13015
+ '</div>' +
13016
+ '<div class="inspect-section">' +
13017
+ '<h4>Policy summary</h4>' +
13018
+ policySection +
13019
+ '</div>' +
13020
+ '<div class="inspect-section">' +
13021
+ '<h4>Identity</h4>' +
13022
+ '<div class="policy-line"><span class="k">Agent id</span><span class="v">' + escHtml(agent.agent_id) + '</span></div>' +
13023
+ '<div class="policy-line"><span class="k">Harness</span><span class="v">' + escHtml(agent.harness) + '</span></div>' +
13024
+ modelLine +
13025
+ '<div class="policy-line"><span class="k">Wrapped at</span><span class="v">' + escHtml(shortTime(agent.wrapped_at)) + '</span></div>' +
13026
+ '</div>' +
13027
+ '<p class="muted" style="margin-top:10px;font-size:12px;">' +
13028
+ '<a href="#activity?agent=' + escHtml(agent.agent_id) + '">View full activity</a> · ' +
13029
+ '<a href="#policy">Edit policy</a>' +
13030
+ '</p>' +
12673
13031
  '</div>' +
12674
- errorBanner +
12675
- '<h3>Pending approvals</h3>' +
12676
- approvalsHtml +
12677
- '<h3 style="margin-top:14px;">Recent activity</h3>' +
12678
- activityHtml +
12679
- '<h3 style="margin-top:14px;">Policy</h3>' +
12680
- '<dl class="kv">' + policyLine + '</dl>' +
12681
- '<p class="muted" style="margin-top:10px;font-size:12px;">' +
12682
- '<a href="#activity?agent=' + escHtml(agent.agent_id) + '">View full activity</a> · ' +
12683
- '<a href="#policy">Edit policy</a>' +
12684
- '</p>' +
12685
13032
  '</div>';
12686
13033
  }
12687
13034
 
@@ -12830,6 +13177,16 @@ function statusDotClass(status) {
12830
13177
  return "red";
12831
13178
  }
12832
13179
 
13180
+ // Card-grid polish (Sprint Piece 2 PR 3) maps the badge dot class onto
13181
+ // the shaped glyph token. Sage circle for ok, ochre triangle for warn,
13182
+ // rust diamond for fail. Keep aligned with .status-glyph rules in
13183
+ // html.ts and the .intel-card-status modifier classes.
13184
+ function statusGlyphClass(dotClass) {
13185
+ if (dotClass === "green") return "ok";
13186
+ if (dotClass === "yellow") return "warn";
13187
+ return "fail";
13188
+ }
13189
+
12833
13190
  function statusLabel(health) {
12834
13191
  if (health === "ok") return "Working";
12835
13192
  if (health === "degraded") return "Degraded";
@@ -12869,13 +13226,18 @@ function renderIntelligenceCenter() {
12869
13226
  }
12870
13227
  const status = state.intelligence.status;
12871
13228
  const config = state.intelligence.config || {};
12872
- const surfaceRows = SURFACES_ORDER.map(function (surfaceId) {
13229
+ const surfaceCards = SURFACES_ORDER.map(function (surfaceId) {
12873
13230
  const surfaceStatus = (status.surfaces || []).find(function (s) { return s.surface === surfaceId; });
12874
13231
  if (!surfaceStatus) {
12875
- return '<div class="intel-row"><div class="intel-row-name">' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) +
12876
- '<small>' + escHtml(surfaceId) + '</small></div>' +
12877
- '<div class="intel-row-body muted">No status reported.</div>' +
12878
- '<div></div></div>';
13232
+ return '<div class="intel-row intel-card" data-intel-surface="' + escHtml(surfaceId) + '">' +
13233
+ '<div class="intel-card-head">' +
13234
+ '<div class="intel-card-name">' +
13235
+ '<strong>' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) + '</strong>' +
13236
+ '<small>' + escHtml(surfaceId) + '</small>' +
13237
+ '</div>' +
13238
+ '</div>' +
13239
+ '<div class="muted">No status reported.</div>' +
13240
+ '</div>';
12879
13241
  }
12880
13242
  const substrate = surfaceStatus.chosen;
12881
13243
  const localPick = (config.local_model_picks || {})[surfaceId];
@@ -12893,41 +13255,62 @@ function renderIntelligenceCenter() {
12893
13255
  if (provider) currentBadge = currentBadge + " (" + (FRONTIER_PROVIDER_LABELS[provider] || provider) + ")";
12894
13256
  }
12895
13257
  const dotClass = statusDotClass((surfaceStatus.badge || {}).status || "red");
13258
+ const glyphClass = statusGlyphClass(dotClass);
12896
13259
  const failures = surfaceStatus.recentFailures || [];
12897
13260
  const expanded = !!state.intelligence.expandedFailures[surfaceId];
12898
- let failuresBlock = "";
13261
+
13262
+ // Card foot. The failures toggle is the load-bearing affordance for
13263
+ // the rc.6 ZZ test (button[data-action="intel-failures-toggle"] with
13264
+ // text "recent failures (N)"). Pluralization is "failures" regardless
13265
+ // of N for backward compatibility with the seeded test contract.
13266
+ // Surface zero-failure state as a quiet mono note so the card still
13267
+ // has visual rhythm in its foot row.
13268
+ let footHtml;
12899
13269
  if (failures.length > 0) {
12900
13270
  const toggleLabel = (expanded ? "Hide" : "View") + " recent failures (" + failures.length + ")";
12901
- const list = expanded
12902
- ? '<ul class="intel-row-failures-list">' +
12903
- failures.slice().reverse().map(function (f) {
12904
- return '<li><span class="muted mono">' + escHtml(shortTime(f.ts)) + '</span> ' +
12905
- '<span class="pill warn">' + escHtml(f.failureClass) + '</span> ' +
12906
- '<span class="muted">' + escHtml(f.snippet) + '</span></li>';
12907
- }).join("") +
12908
- '</ul>'
12909
- : '';
12910
- failuresBlock =
12911
- '<div class="intel-row-failures">' +
12912
- '<button class="btn btn-link" data-action="intel-failures-toggle" data-intel-surface="' + escHtml(surfaceId) + '">' +
12913
- escHtml(toggleLabel) +
12914
- '</button>' +
12915
- list +
13271
+ footHtml =
13272
+ '<button class="intel-failures-toggle' + (expanded ? ' open' : '') + '" data-action="intel-failures-toggle" data-intel-surface="' + escHtml(surfaceId) + '">' +
13273
+ '<span class="caret"></span>' +
13274
+ escHtml(toggleLabel) +
13275
+ '</button>';
13276
+ } else {
13277
+ footHtml = '<span class="muted mono" style="font-size: 11px;">no recent failures</span>';
13278
+ }
13279
+
13280
+ let failuresBlock = "";
13281
+ if (failures.length > 0 && expanded) {
13282
+ const rows = failures.slice().reverse().map(function (f) {
13283
+ return '<div class="intel-failure-row">' +
13284
+ '<span class="ts">' + escHtml(shortTime(f.ts)) + '</span>' +
13285
+ '<div>' +
13286
+ '<div class="err-class">' + escHtml(f.failureClass) + '</div>' +
13287
+ '<div>' + escHtml(f.snippet) + '</div>' +
13288
+ '</div>' +
12916
13289
  '</div>';
13290
+ }).join("");
13291
+ failuresBlock = '<div class="intel-failures">' + rows + '</div>';
12917
13292
  }
12918
- return '<div class="intel-row" data-intel-surface="' + escHtml(surfaceId) + '">' +
12919
- '<div class="intel-row-name">' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) +
12920
- '<small>' + escHtml(surfaceId) + '</small></div>' +
12921
- '<div class="intel-row-body">' +
12922
- '<div class="intel-row-current">' +
12923
- '<span class="intel-status-dot ' + dotClass + '" title="' + escHtml(statusLabel(surfaceStatus.health)) + '"></span>' +
12924
- '<span class="pill">' + escHtml(currentBadge) + '</span>' +
12925
- '<span class="muted mono">' + escHtml(statusLabel(surfaceStatus.health)) + '</span>' +
13293
+
13294
+ return '<div class="intel-row intel-card" data-intel-surface="' + escHtml(surfaceId) + '">' +
13295
+ '<div class="intel-card-head">' +
13296
+ '<div class="intel-card-name">' +
13297
+ '<strong>' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) + '</strong>' +
13298
+ '<small>' + escHtml(surfaceId) + '</small>' +
12926
13299
  '</div>' +
12927
- '<div class="intel-row-tradeoff">' + escHtml(substrateTradeoff(substrate)) + '</div>' +
12928
- failuresBlock +
13300
+ '<span class="intel-card-status ' + glyphClass + '" title="' + escHtml(statusLabel(surfaceStatus.health)) + '">' +
13301
+ '<span class="status-glyph ' + glyphClass + '"></span>' +
13302
+ escHtml(statusLabel(surfaceStatus.health)) +
13303
+ '</span>' +
12929
13304
  '</div>' +
12930
- '<div><button class="btn" data-action="intel-picker-open" data-intel-surface="' + escHtml(surfaceId) + '">Change</button></div>' +
13305
+ '<div class="intel-substrate">' +
13306
+ '<div class="sub-line primary">' +
13307
+ '<span>' + escHtml(currentBadge) + '</span>' +
13308
+ '<button class="btn-quiet" data-action="intel-picker-open" data-intel-surface="' + escHtml(surfaceId) + '">Change</button>' +
13309
+ '</div>' +
13310
+ '</div>' +
13311
+ '<div class="intel-row-tradeoff">' + escHtml(substrateTradeoff(substrate)) + '</div>' +
13312
+ '<div class="intel-card-foot">' + footHtml + '</div>' +
13313
+ failuresBlock +
12931
13314
  '</div>';
12932
13315
  }).join("\n");
12933
13316
 
@@ -12939,11 +13322,15 @@ function renderIntelligenceCenter() {
12939
13322
 
12940
13323
  const modal = state.intelligence.picker.open ? renderIntelligencePicker() : "";
12941
13324
 
12942
- return '<section class="intel-center">' +
12943
- '<p class="eyebrow">INTELLIGENCE</p>' +
12944
- '<h1>Intelligence Substrate</h1>' +
12945
- '<p class="intel-subtitle">Choose how Sanctuary thinks. Tradeoffs visible per surface. Multi-option framing is preserved: no single substrate is the right answer.</p>' +
12946
- '<section class="intel-panel"><h2>Surfaces</h2>' + surfaceRows + '</section>' +
13325
+ return '<section class="intel-wrap">' +
13326
+ '<div class="page-head">' +
13327
+ '<div>' +
13328
+ '<p class="eyebrow">Intelligence</p>' +
13329
+ '<h1>Substrate routing.</h1>' +
13330
+ '<p class="sub">Six surfaces, six choices. Each surface picks where its thinking happens. Local for privacy. Hosted for capability. Hybrid for both.</p>' +
13331
+ '</div>' +
13332
+ '</div>' +
13333
+ '<div class="intel-grid">' + surfaceCards + '</div>' +
12947
13334
  '<section class="intel-panel"><h2>Host capability</h2>' +
12948
13335
  '<dl class="intel-hardware">' +
12949
13336
  '<dt>Total RAM</dt><dd>' + escHtml(hardware.totalRamGb || "?") + ' GB</dd>' +
@@ -13736,6 +14123,13 @@ document.addEventListener("click", function (ev) {
13736
14123
  const intelLocalModel = tgt.getAttribute("data-intel-local-model");
13737
14124
  const intelFrontierProvider = tgt.getAttribute("data-intel-frontier-provider");
13738
14125
  if (action === "lockdown") return void onLockdownClick();
14126
+ if (action === "theme-toggle") {
14127
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
14128
+ const next = isDark ? "light" : "dark";
14129
+ sessionStorage.setItem(THEME_KEY, next);
14130
+ applyTheme(next);
14131
+ return;
14132
+ }
13739
14133
  if (action === "intel-reload") { return void fetchIntelligenceState().then(rerender); }
13740
14134
  if (action === "intel-picker-open" && intelSurface) return void onIntelPickerOpen(intelSurface);
13741
14135
  if (action === "intel-picker-close") return onIntelPickerClose();
@@ -13889,12 +14283,30 @@ document.addEventListener("input", function (ev) {
13889
14283
  // then the dashboard view renders a static welcome card with no form
13890
14284
  // inputs that could be confused for a working command surface.
13891
14285
 
13892
- // Theme: system preference only at v1.1.
14286
+ // Theme: explicit operator preference (sessionStorage) overrides system
14287
+ // pref. The toggle button in the topbar dispatches data-action
14288
+ // "theme-toggle" which writes the chosen theme and updates the
14289
+ // [data-theme] attribute. When no explicit choice exists, fall back to
14290
+ // system preference and track changes so dark-mode-at-sunset behavior
14291
+ // keeps working on macOS / Windows.
14292
+ const THEME_KEY = "sanctuary-v11-theme";
14293
+ function applyTheme(theme) {
14294
+ if (theme === "dark") document.documentElement.setAttribute("data-theme", "dark");
14295
+ else document.documentElement.removeAttribute("data-theme");
14296
+ }
14297
+ const explicitTheme = sessionStorage.getItem(THEME_KEY);
13893
14298
  const mq = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
13894
- if (mq && mq.matches) document.documentElement.setAttribute("data-theme", "dark");
14299
+ if (explicitTheme === "dark" || explicitTheme === "light") {
14300
+ applyTheme(explicitTheme);
14301
+ } else if (mq && mq.matches) {
14302
+ applyTheme("dark");
14303
+ }
13895
14304
  if (mq) mq.addEventListener("change", function (e) {
13896
- if (e.matches) document.documentElement.setAttribute("data-theme", "dark");
13897
- else document.documentElement.removeAttribute("data-theme");
14305
+ // Only honor system pref changes when the operator has not made an
14306
+ // explicit choice. Once they toggle, the choice sticks for the
14307
+ // session.
14308
+ if (sessionStorage.getItem(THEME_KEY)) return;
14309
+ applyTheme(e.matches ? "dark" : "light");
13898
14310
  });
13899
14311
 
13900
14312
  // Boot.
@@ -13930,6 +14342,26 @@ var STYLES = String.raw`:root {
13930
14342
  --rad: 6px;
13931
14343
  --rad-lg: 10px;
13932
14344
  --shadow: 0 1px 2px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.02);
14345
+ /* Type scale. Names are size-relative, not semantic, so refactors do
14346
+ not need to invent new names. Existing rules used these literal px
14347
+ values; tokens make future polish a one-line change. */
14348
+ --text-xs: 11px;
14349
+ --text-sm: 12px;
14350
+ --text-base: 13px;
14351
+ --text-md: 14px;
14352
+ --text-lg: 16px;
14353
+ --text-xl: 22px;
14354
+ --text-display: 36px;
14355
+ /* Spacing scale (4px multiples). Layout-specific magic numbers
14356
+ (220px sidebar, 360px fortress rail, concierge-card heights) stay
14357
+ as literals because the token system is for component padding /
14358
+ margin / gap, not grid track sizing. */
14359
+ --space-1: 4px;
14360
+ --space-2: 8px;
14361
+ --space-3: 12px;
14362
+ --space-4: 16px;
14363
+ --space-5: 24px;
14364
+ --space-6: 32px;
13933
14365
  }
13934
14366
  [data-theme="dark"] {
13935
14367
  --paper: #121210;
@@ -13956,7 +14388,7 @@ var STYLES = String.raw`:root {
13956
14388
  html, body { margin: 0; padding: 0; }
13957
14389
  body {
13958
14390
  font-family: var(--sans);
13959
- font-size: 14px;
14391
+ font-size: var(--text-md);
13960
14392
  background: var(--paper);
13961
14393
  color: var(--ink);
13962
14394
  line-height: 1.45;
@@ -13977,20 +14409,27 @@ body {
13977
14409
  "sidebar main";
13978
14410
  }
13979
14411
  .sidebar { grid-area: sidebar; background: var(--paper-2); border-right: 1px solid var(--rule); padding: 12px 8px; }
13980
- .sidebar h1 { font-family: var(--serif); font-size: 16px; margin: 4px 8px 16px; }
14412
+ .sidebar h1 { font-family: var(--serif); font-size: var(--text-lg); margin: 4px 8px 16px; }
13981
14413
  .sidebar nav { display: flex; flex-direction: column; gap: 2px; }
13982
14414
  .sidebar nav a {
13983
- display: block; padding: 6px 10px; border-radius: var(--rad);
13984
- color: var(--ink-2); text-decoration: none; font-size: 13px;
14415
+ display: flex; align-items: center; gap: var(--space-2);
14416
+ padding: 6px 10px; border-radius: var(--rad);
14417
+ color: var(--ink-2); text-decoration: none; font-size: var(--text-base);
14418
+ }
14419
+ .sidebar nav a svg {
14420
+ flex-shrink: 0; width: 16px; height: 16px;
14421
+ color: var(--ink-3);
13985
14422
  }
13986
14423
  .sidebar nav a:hover { background: var(--paper-3); }
14424
+ .sidebar nav a:hover svg { color: var(--ink-2); }
13987
14425
  .sidebar nav a.active { background: var(--surface); color: var(--ink); border: 1px solid var(--rule); }
13988
- .topbar { grid-area: topbar; display: flex; align-items: center; gap: 12px; padding: 0 16px; border-bottom: 1px solid var(--rule); background: var(--surface); }
13989
- .topbar .brand { font-family: var(--serif); font-size: 14px; }
14426
+ .sidebar nav a.active svg { color: var(--ink); }
14427
+ .topbar { grid-area: topbar; display: flex; align-items: center; gap: var(--space-3); padding: 0 16px; border-bottom: 1px solid var(--rule); background: var(--surface); }
14428
+ .topbar .brand { font-family: var(--serif); font-size: var(--text-md); }
13990
14429
  .topbar .pills { display: flex; gap: 6px; flex: 1; }
13991
14430
  .pill {
13992
14431
  display: inline-flex; align-items: center; gap: 4px;
13993
- padding: 2px 8px; border-radius: 12px; font-size: 11px;
14432
+ padding: 2px 8px; border-radius: 12px; font-size: var(--text-xs);
13994
14433
  font-family: var(--mono); border: 1px solid var(--rule);
13995
14434
  background: var(--surface-2); color: var(--ink-2);
13996
14435
  }
@@ -14002,7 +14441,7 @@ body {
14002
14441
  display: inline-flex; align-items: center; gap: 4px;
14003
14442
  padding: 4px 10px; border-radius: var(--rad);
14004
14443
  background: var(--surface); border: 1px solid var(--rule);
14005
- font-family: var(--sans); font-size: 12px; color: var(--ink);
14444
+ font-family: var(--sans); font-size: var(--text-sm); color: var(--ink);
14006
14445
  cursor: pointer;
14007
14446
  }
14008
14447
  .btn:hover:not(:disabled) { background: var(--surface-2); }
@@ -14012,21 +14451,30 @@ body {
14012
14451
  .btn.btn-danger { background: var(--rust-bg); color: var(--rust); border-color: var(--rust); }
14013
14452
  .btn.tier1-pending { background: var(--ochre-bg); color: var(--ochre); border-color: var(--ochre); }
14014
14453
  .btn.tier1-engaged { background: var(--rust-bg); color: var(--rust); border-color: var(--rust); }
14454
+ .btn.btn-icon {
14455
+ padding: 4px 6px;
14456
+ display: inline-flex; align-items: center; justify-content: center;
14457
+ color: var(--ink-2);
14458
+ }
14459
+ .btn.btn-icon svg { width: 16px; height: 16px; }
14460
+ .btn.btn-icon .icon-sun { display: none; }
14461
+ [data-theme="dark"] .btn.btn-icon .icon-moon { display: none; }
14462
+ [data-theme="dark"] .btn.btn-icon .icon-sun { display: inline; }
14015
14463
  .main { grid-area: main; overflow-y: auto; padding: 16px 24px; }
14016
- .fortress { grid-area: fortress; overflow-y: auto; border-left: 1px solid var(--rule); background: var(--paper-2); padding: 12px; display: flex; flex-direction: column; gap: 10px; }
14464
+ .fortress { grid-area: fortress; overflow-y: auto; border-left: 1px solid var(--rule); background: var(--paper-2); padding: var(--space-3); display: flex; flex-direction: column; gap: 10px; }
14017
14465
  .app.route-full .fortress { display: none; }
14018
14466
  .card {
14019
14467
  background: var(--surface); border: 1px solid var(--rule);
14020
- border-radius: var(--rad); padding: 12px;
14468
+ border-radius: var(--rad); padding: var(--space-3);
14021
14469
  }
14022
- .card h3 { margin: 0 0 8px; font-size: 13px; font-weight: 600; color: var(--ink); }
14470
+ .card h3 { margin: 0 0 8px; font-size: var(--text-base); font-weight: 600; color: var(--ink); }
14023
14471
  .muted { color: var(--ink-3); }
14024
- .mono { font-family: var(--mono); font-size: 12px; }
14025
- .row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px dashed var(--rule); }
14472
+ .mono { font-family: var(--mono); font-size: var(--text-sm); }
14473
+ .row { display: flex; align-items: center; gap: var(--space-2); padding: 6px 0; border-bottom: 1px dashed var(--rule); }
14026
14474
  .row:last-child { border-bottom: 0; }
14027
14475
  .row .grow { flex: 1; min-width: 0; }
14028
14476
  .agent-row { flex-direction: column; align-items: stretch; gap: 6px; }
14029
- .agent-row-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
14477
+ .agent-row-head { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
14030
14478
  .agent-row-head .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
14031
14479
  .agent-row-actions { display: flex; flex-wrap: wrap; gap: 4px; }
14032
14480
  /* Click-to-inspect affordance: the head sub-row of a fortress-column
@@ -14036,7 +14484,7 @@ body {
14036
14484
  .agent-row-head[data-action="agent-row-inspect-open"] { cursor: pointer; border-radius: var(--rad); padding: 4px 6px; margin: -4px -6px; }
14037
14485
  .agent-row-head[data-action="agent-row-inspect-open"]:hover { background: var(--paper-3); }
14038
14486
  .agent-row-head[data-action="agent-row-inspect-open"]:focus-visible { outline: 2px solid var(--ink-3); outline-offset: 1px; }
14039
- .kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: 12px; }
14487
+ .kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: var(--text-sm); }
14040
14488
  .kv dt { color: var(--ink-3); }
14041
14489
  .kv dd { margin: 0; color: var(--ink); }
14042
14490
  .glyph { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--ink-4); }
@@ -14047,74 +14495,74 @@ body {
14047
14495
  .toast {
14048
14496
  position: fixed; bottom: 16px; right: 16px;
14049
14497
  background: var(--ink); color: var(--paper); padding: 8px 12px;
14050
- border-radius: var(--rad); font-size: 12px; z-index: 1000;
14498
+ border-radius: var(--rad); font-size: var(--text-sm); z-index: 1000;
14051
14499
  max-width: 360px;
14052
14500
  }
14053
14501
  .toast.error { background: var(--rust); color: var(--paper); }
14054
- .layer-card { background: var(--surface-2); border: 1px solid var(--rule); border-radius: var(--rad); padding: 8px; }
14055
- .layer-card h4 { margin: 0 0 4px; font-size: 12px; font-weight: 600; }
14056
- .layer-card p { margin: 0; font-size: 11px; color: var(--ink-3); }
14057
- .chat-thread { display: flex; flex-direction: column; gap: 8px; padding-bottom: 12px; }
14502
+ .layer-card { background: var(--surface-2); border: 1px solid var(--rule); border-radius: var(--rad); padding: var(--space-2); }
14503
+ .layer-card h4 { margin: 0 0 4px; font-size: var(--text-sm); font-weight: 600; }
14504
+ .layer-card p { margin: 0; font-size: var(--text-xs); color: var(--ink-3); }
14505
+ .chat-thread { display: flex; flex-direction: column; gap: var(--space-2); padding-bottom: 12px; }
14058
14506
  .chat-msg { padding: 8px 10px; border-radius: var(--rad); border: 1px solid var(--rule); background: var(--surface); max-width: 78%; }
14059
- .chat-msg.system { background: var(--paper-3); color: var(--ink-3); font-size: 12px; max-width: 100%; }
14507
+ .chat-msg.system { background: var(--paper-3); color: var(--ink-3); font-size: var(--text-sm); max-width: 100%; }
14060
14508
  .chat-msg.agent { align-self: flex-start; }
14061
14509
  .chat-msg.operator { align-self: flex-end; background: var(--ink); color: var(--paper); }
14062
14510
  .chat-msg .meta { font-size: 10px; color: var(--ink-4); margin-top: 4px; }
14063
- .composer { display: flex; gap: 8px; padding: 8px; border-top: 1px solid var(--rule); }
14511
+ .composer { display: flex; gap: var(--space-2); padding: var(--space-2); border-top: 1px solid var(--rule); }
14064
14512
  .composer input { flex: 1; padding: 6px 8px; border: 1px solid var(--rule); border-radius: var(--rad); font-family: var(--sans); }
14065
14513
  .wizard-step { padding: 10px; border: 1px solid var(--rule); border-radius: var(--rad); margin-bottom: 8px; background: var(--surface); }
14066
14514
  .wizard-step.active { border-color: var(--ink); }
14067
14515
  .wizard-step.done { background: var(--sage-bg); border-color: var(--sage); }
14068
- .code-block { font-family: var(--mono); background: var(--paper-3); padding: 8px; border-radius: var(--rad); font-size: 12px; overflow-x: auto; }
14516
+ .code-block { font-family: var(--mono); background: var(--paper-3); padding: var(--space-2); border-radius: var(--rad); font-size: var(--text-sm); overflow-x: auto; }
14069
14517
  .policy-center { max-width: 980px; margin: 0 auto; }
14070
- .policy-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: 12px; letter-spacing: 0; }
14071
- .policy-center h1 { font-family: var(--serif); font-size: 36px; line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
14518
+ .policy-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: var(--text-sm); letter-spacing: 0; }
14519
+ .policy-center h1 { font-family: var(--serif); font-size: var(--text-display); line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
14072
14520
  .policy-subtitle { max-width: 860px; color: var(--ink-2); font-size: 15px; margin: 0 0 24px; }
14073
14521
  .policy-panel { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--rad-lg); padding: 20px; margin: 18px 0; }
14074
- .policy-panel h2 { font-family: var(--serif); font-size: 22px; font-weight: 400; margin: 0 0 14px; }
14075
- .template-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
14522
+ .policy-panel h2 { font-family: var(--serif); font-size: var(--text-xl); font-weight: 400; margin: 0 0 14px; }
14523
+ .template-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
14076
14524
  .template-card { background: var(--surface-2); border: 1px solid var(--rule); border-radius: var(--rad); padding: 14px; min-height: 132px; }
14077
- .template-card-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 12px; }
14078
- .severity { border-radius: 999px; padding: 2px 9px; font-family: var(--mono); font-size: 11px; font-weight: 700; }
14525
+ .template-card-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-2); margin-bottom: 12px; }
14526
+ .severity { border-radius: 999px; padding: 2px 9px; font-family: var(--mono); font-size: var(--text-xs); font-weight: 700; }
14079
14527
  .severity.low { color: var(--sage); background: var(--sage-bg); }
14080
14528
  .severity.medium { color: var(--ochre); background: var(--ochre-bg); }
14081
14529
  .template-id { background: var(--paper-3); border-radius: var(--rad); padding: 2px 7px; color: var(--ink-3); }
14082
- .template-card h3 { font-size: 16px; margin: 0 0 6px; }
14083
- .template-card p { color: var(--ink-3); margin: 0; font-size: 14px; }
14530
+ .template-card h3 { font-size: var(--text-lg); margin: 0 0 6px; }
14531
+ .template-card p { color: var(--ink-3); margin: 0; font-size: var(--text-md); }
14084
14532
  .rules-scroll { overflow-x: auto; }
14085
14533
  .rules-table { width: 100%; border-collapse: collapse; min-width: 760px; }
14086
- .rules-table th { text-align: left; color: var(--ink-3); font-family: var(--mono); font-size: 12px; letter-spacing: 0; padding: 8px 10px; border-bottom: 1px solid var(--rule); }
14534
+ .rules-table th { text-align: left; color: var(--ink-3); font-family: var(--mono); font-size: var(--text-sm); letter-spacing: 0; padding: 8px 10px; border-bottom: 1px solid var(--rule); }
14087
14535
  .rules-table td { padding: 12px 10px; border-bottom: 1px solid var(--rule); vertical-align: top; }
14088
14536
  .link-btn, .template-cell { border: 0; background: transparent; color: var(--ink); padding: 0; cursor: pointer; font: inherit; text-align: left; }
14089
14537
  .template-cell { font-family: var(--mono); max-width: 180px; overflow-wrap: anywhere; }
14090
14538
  .template-picker { position: absolute; z-index: 20; margin-top: 8px; width: min(420px, calc(100vw - 80px)); background: var(--surface); border: 1px solid var(--rule-2); border-radius: var(--rad); box-shadow: var(--shadow); padding: 10px; }
14091
14539
  .template-picker-options { display: grid; gap: 6px; max-height: 320px; overflow-y: auto; }
14092
- .template-option { display: grid; grid-template-columns: 18px 1fr; gap: 8px; padding: 8px; border: 1px solid var(--rule); border-radius: var(--rad); background: var(--surface-2); }
14540
+ .template-option { display: grid; grid-template-columns: 18px 1fr; gap: var(--space-2); padding: var(--space-2); border: 1px solid var(--rule); border-radius: var(--rad); background: var(--surface-2); }
14093
14541
  .template-option small { display: block; color: var(--ink-3); margin-top: 2px; }
14094
- .template-picker-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
14542
+ .template-picker-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: 10px; }
14095
14543
  .allow-count { color: var(--sage); font-weight: 700; }
14096
14544
  .block-count { color: var(--rust); }
14097
14545
  .toggle-on { display: inline-block; width: 28px; height: 16px; border-radius: 999px; background: var(--sage); position: relative; }
14098
14546
  .toggle-on::after { content: ""; position: absolute; right: 2px; top: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--surface); }
14099
14547
  .error-text { color: var(--rust); margin: 8px 0 0; }
14100
14548
  .intel-center { max-width: 980px; margin: 0 auto; }
14101
- .intel-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: 12px; letter-spacing: 0; }
14102
- .intel-center h1 { font-family: var(--serif); font-size: 36px; line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
14549
+ .intel-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: var(--text-sm); letter-spacing: 0; }
14550
+ .intel-center h1 { font-family: var(--serif); font-size: var(--text-display); line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
14103
14551
  .intel-subtitle { max-width: 860px; color: var(--ink-2); font-size: 15px; margin: 0 0 24px; }
14104
14552
  .intel-panel { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--rad-lg); padding: 20px; margin: 18px 0; }
14105
- .intel-panel h2 { font-family: var(--serif); font-size: 22px; font-weight: 400; margin: 0 0 14px; }
14106
- .intel-row { display: grid; grid-template-columns: 200px 1fr auto; gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--rule); align-items: start; }
14553
+ .intel-panel h2 { font-family: var(--serif); font-size: var(--text-xl); font-weight: 400; margin: 0 0 14px; }
14554
+ .intel-row { display: grid; grid-template-columns: 200px 1fr auto; gap: var(--space-4); padding: 14px 0; border-bottom: 1px solid var(--rule); align-items: start; }
14107
14555
  .intel-row:last-child { border-bottom: 0; }
14108
14556
  .intel-row-name { font-weight: 600; }
14109
- .intel-row-name small { display: block; color: var(--ink-3); font-weight: 400; font-size: 12px; margin-top: 2px; font-family: var(--mono); }
14557
+ .intel-row-name small { display: block; color: var(--ink-3); font-weight: 400; font-size: var(--text-sm); margin-top: 2px; font-family: var(--mono); }
14110
14558
  .intel-row-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
14111
- .intel-row-current { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
14112
- .intel-row-tradeoff { color: var(--ink-2); font-size: 13px; line-height: 1.5; }
14559
+ .intel-row-current { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }
14560
+ .intel-row-tradeoff { color: var(--ink-2); font-size: var(--text-base); line-height: 1.5; }
14113
14561
  .intel-status-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
14114
14562
  .intel-status-dot.green { background: var(--sage); }
14115
14563
  .intel-status-dot.yellow { background: var(--ochre); }
14116
14564
  .intel-status-dot.red { background: var(--rust); }
14117
- .intel-hardware { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; font-size: 13px; }
14565
+ .intel-hardware { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; font-size: var(--text-base); }
14118
14566
  .intel-hardware dt { color: var(--ink-3); font-family: var(--mono); }
14119
14567
  .intel-hardware dd { margin: 0; }
14120
14568
  .intel-modal-backdrop {
@@ -14127,33 +14575,162 @@ body {
14127
14575
  box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 24px;
14128
14576
  width: 100%; max-width: 640px;
14129
14577
  }
14130
- .intel-modal h2 { font-family: var(--serif); font-size: 22px; font-weight: 400; margin: 0 0 8px; }
14131
- .intel-modal-subtitle { color: var(--ink-3); margin: 0 0 18px; font-size: 13px; }
14578
+ .intel-modal h2 { font-family: var(--serif); font-size: var(--text-xl); font-weight: 400; margin: 0 0 8px; }
14579
+ .intel-modal-subtitle { color: var(--ink-3); margin: 0 0 18px; font-size: var(--text-base); }
14132
14580
  .intel-option {
14133
- border: 1px solid var(--rule); border-radius: var(--rad); padding: 12px;
14581
+ border: 1px solid var(--rule); border-radius: var(--rad); padding: var(--space-3);
14134
14582
  margin-bottom: 10px; background: var(--surface-2); cursor: pointer;
14135
14583
  display: grid; grid-template-columns: 18px 1fr; gap: 10px; align-items: start;
14136
14584
  }
14137
14585
  .intel-option.selected { border-color: var(--ink); background: var(--surface); }
14138
- .intel-option-body strong { display: block; font-size: 14px; margin-bottom: 4px; }
14139
- .intel-option-body small { display: block; color: var(--ink-3); font-size: 12px; line-height: 1.5; }
14586
+ .intel-option-body strong { display: block; font-size: var(--text-md); margin-bottom: 4px; }
14587
+ .intel-option-body small { display: block; color: var(--ink-3); font-size: var(--text-sm); line-height: 1.5; }
14140
14588
  .intel-suboptions { margin-top: 10px; padding: 10px; background: var(--paper-3); border-radius: var(--rad); }
14141
- .intel-suboptions label { display: block; margin: 6px 0; font-size: 13px; }
14589
+ .intel-suboptions label { display: block; margin: 6px 0; font-size: var(--text-base); }
14142
14590
  .intel-suboptions input[type="text"], .intel-suboptions input[type="password"] {
14143
14591
  width: 100%; padding: 6px 8px; border: 1px solid var(--rule); border-radius: var(--rad);
14144
- font-family: var(--mono); font-size: 12px; box-sizing: border-box;
14592
+ font-family: var(--mono); font-size: var(--text-sm); box-sizing: border-box;
14593
+ }
14594
+ .intel-modal-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: 18px; }
14595
+ /*
14596
+ * Intelligence panel polish (Sprint Piece 2 PR 3). Card grid layout
14597
+ * replaces the legacy 3-col row visual. The legacy .intel-row and
14598
+ * .intel-status-dot rules above are retained for the e2e selector
14599
+ * contract (.intel-row[data-intel-surface="..."]) and as the responsive
14600
+ * fallback. Cards render as a flex column with a substrate inset,
14601
+ * status badge with shaped glyph, and a recent-failures toggle in the
14602
+ * card foot.
14603
+ */
14604
+ .intel-wrap { max-width: 1000px; margin: 0 auto; }
14605
+ .intel-grid {
14606
+ display: grid;
14607
+ grid-template-columns: repeat(2, minmax(0, 1fr));
14608
+ gap: 12px;
14609
+ }
14610
+ .intel-card {
14611
+ background: var(--surface);
14612
+ border: 1px solid var(--rule);
14613
+ border-radius: var(--rad-lg);
14614
+ padding: 16px;
14615
+ display: flex; flex-direction: column;
14616
+ gap: 12px;
14617
+ }
14618
+ .intel-card-head {
14619
+ display: flex; align-items: flex-start; justify-content: space-between;
14620
+ gap: 10px;
14621
+ }
14622
+ .intel-card-name {
14623
+ display: flex; flex-direction: column; gap: 2px;
14624
+ min-width: 0;
14625
+ }
14626
+ .intel-card-name strong {
14627
+ font-family: var(--serif); font-weight: 500;
14628
+ font-size: 15px; letter-spacing: 0.005em;
14629
+ }
14630
+ .intel-card-name small {
14631
+ color: var(--ink-3); font-size: 11px; font-family: var(--mono);
14632
+ }
14633
+ .intel-card-status {
14634
+ display: inline-flex; align-items: center; gap: 6px;
14635
+ font-family: var(--mono); font-size: 11px;
14636
+ padding: 3px 9px; border-radius: 999px;
14637
+ border: 1px solid var(--rule); background: var(--surface-2);
14638
+ flex-shrink: 0;
14639
+ }
14640
+ .intel-card-status.ok { color: var(--sage); border-color: var(--sage); background: var(--sage-bg); }
14641
+ .intel-card-status.warn { color: var(--ochre); border-color: var(--ochre); background: var(--ochre-bg); }
14642
+ .intel-card-status.fail { color: var(--rust); border-color: var(--rust); background: var(--rust-bg); }
14643
+ .status-glyph {
14644
+ width: 10px; height: 10px;
14645
+ position: relative; flex-shrink: 0;
14145
14646
  }
14146
- .intel-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
14647
+ .status-glyph.ok::before {
14648
+ content: ""; position: absolute; inset: 0;
14649
+ border-radius: 50%; background: currentColor;
14650
+ }
14651
+ .status-glyph.warn::before {
14652
+ content: ""; position: absolute; inset: 0;
14653
+ background: currentColor;
14654
+ clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
14655
+ }
14656
+ .status-glyph.fail::before {
14657
+ content: ""; position: absolute; inset: 1px;
14658
+ background: currentColor;
14659
+ clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
14660
+ }
14661
+ .intel-substrate {
14662
+ display: flex; flex-direction: column; gap: 4px;
14663
+ padding: 10px 12px;
14664
+ background: var(--paper-2);
14665
+ border: 1px solid var(--rule);
14666
+ border-radius: var(--rad);
14667
+ }
14668
+ .intel-substrate .sub-line {
14669
+ display: flex; justify-content: space-between; align-items: center;
14670
+ gap: 10px; font-size: 12px;
14671
+ }
14672
+ .intel-substrate .sub-line.primary {
14673
+ font-family: var(--mono); font-size: 13px; color: var(--ink);
14674
+ }
14675
+ .intel-substrate .sub-line.secondary { color: var(--ink-3); font-size: 11px; }
14676
+ .intel-card-foot {
14677
+ display: flex; align-items: center; justify-content: space-between;
14678
+ gap: 8px; padding-top: 4px;
14679
+ }
14680
+ .intel-failures-toggle {
14681
+ display: inline-flex; align-items: center; gap: 6px;
14682
+ font-size: 12px; font-family: var(--mono);
14683
+ color: var(--ink-3);
14684
+ background: transparent; border: 0; padding: 0;
14685
+ cursor: pointer;
14686
+ }
14687
+ .intel-failures-toggle:hover { color: var(--ink); }
14688
+ .intel-failures-toggle .caret {
14689
+ display: inline-block; width: 0; height: 0;
14690
+ border-left: 4px solid transparent;
14691
+ border-right: 4px solid transparent;
14692
+ border-top: 5px solid currentColor;
14693
+ transition: transform 160ms ease;
14694
+ }
14695
+ .intel-failures-toggle.open .caret { transform: rotate(180deg); }
14696
+ .intel-failures {
14697
+ border-top: 1px solid var(--rule);
14698
+ padding-top: 12px;
14699
+ display: flex; flex-direction: column;
14700
+ gap: 8px;
14701
+ }
14702
+ .intel-failure-row {
14703
+ display: grid; grid-template-columns: 88px 1fr; gap: 12px;
14704
+ font-size: 12px; padding: 8px 10px;
14705
+ border-radius: var(--rad);
14706
+ background: var(--paper-2);
14707
+ border: 1px solid var(--rule);
14708
+ }
14709
+ .intel-failure-row .ts {
14710
+ font-family: var(--mono); font-size: 11px; color: var(--ink-3);
14711
+ }
14712
+ .intel-failure-row .err-class {
14713
+ font-family: var(--mono); font-size: 10px;
14714
+ letter-spacing: 0.04em; text-transform: uppercase;
14715
+ color: var(--rust); margin-bottom: 2px;
14716
+ }
14717
+ .btn-quiet {
14718
+ background: transparent; border: 1px solid var(--rule);
14719
+ padding: 2px 6px; font-size: 11px;
14720
+ border-radius: var(--rad); cursor: pointer;
14721
+ color: var(--ink-2); font-family: var(--sans);
14722
+ }
14723
+ .btn-quiet:hover { background: var(--surface-2); color: var(--ink); }
14147
14724
  .banner-warn {
14148
14725
  background: var(--ochre-bg); color: var(--ochre); border: 1px solid var(--ochre);
14149
- border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: 13px;
14726
+ border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: var(--text-base);
14150
14727
  }
14151
14728
  .banner-info {
14152
14729
  background: var(--indigo-bg); color: var(--indigo); border: 1px solid var(--indigo);
14153
- border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: 13px;
14730
+ border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: var(--text-base);
14154
14731
  }
14155
14732
  .btn.chip {
14156
- border-radius: 999px; padding: 4px 12px; font-size: 12px;
14733
+ border-radius: 999px; padding: 4px 12px; font-size: var(--text-sm);
14157
14734
  background: var(--surface-2); border-color: var(--rule);
14158
14735
  }
14159
14736
  .btn.chip:hover:not(:disabled) { background: var(--paper-3); }
@@ -14170,85 +14747,210 @@ body {
14170
14747
  * dynamically, but the input box is below the fold, so I still have
14171
14748
  * to scroll." rc.5 closes that with a structural layout fix.
14172
14749
  */
14750
+ /* Sprint Piece 2 PR 2 (2026-05-03): concierge surface polish.
14751
+ * Translates Claude Design references at
14752
+ * server/docs/design-refs/sprint-piece-2/surface-concierge.jsx and the
14753
+ * Surface 1 block of surfaces.css. The bounded-card layout above is
14754
+ * preserved verbatim because rc.5 and the DDD e2e suite depend on it;
14755
+ * the Claude Design reference uses height: 720px for the card, but
14756
+ * production keeps the calc-based bounded height so the card adapts
14757
+ * to the operator viewport. The polish lands the persona glyph-ring,
14758
+ * mono uppercase author labels, paper-2 background for concierge
14759
+ * replies, suggest-grid empty-state cards, and the composer input-wrap
14760
+ * with the keyboard-shortcut pill.
14761
+ */
14762
+ .concierge-wrap { max-width: 880px; margin: 0 auto; }
14763
+ .page-head {
14764
+ display: flex; align-items: flex-end; justify-content: space-between;
14765
+ gap: var(--space-4);
14766
+ margin-bottom: 18px; padding-bottom: 14px;
14767
+ border-bottom: 1px solid var(--rule);
14768
+ }
14769
+ .page-head .eyebrow {
14770
+ font-family: var(--mono); font-size: var(--text-xs);
14771
+ letter-spacing: 0.08em; text-transform: uppercase;
14772
+ color: var(--ink-3);
14773
+ margin: 0 0 6px;
14774
+ }
14775
+ .page-head h1 {
14776
+ font-family: var(--serif); font-weight: 400;
14777
+ font-size: 28px; letter-spacing: -0.01em;
14778
+ margin: 0 0 4px;
14779
+ }
14780
+ .page-head .sub {
14781
+ color: var(--ink-3); margin: 0;
14782
+ font-size: var(--text-base); max-width: 60ch;
14783
+ }
14173
14784
  .concierge-card {
14174
14785
  display: flex; flex-direction: column;
14175
14786
  height: calc(100vh - 180px);
14176
14787
  max-height: calc(100vh - 180px);
14177
14788
  min-height: 360px;
14178
- padding: 16px 18px;
14789
+ padding: 18px 22px 14px;
14179
14790
  gap: 0;
14180
14791
  }
14181
14792
  .concierge-header {
14182
14793
  display: flex; align-items: center; justify-content: space-between;
14183
- gap: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--rule);
14794
+ gap: var(--space-3); padding-bottom: 14px;
14795
+ border-bottom: 1px solid var(--rule);
14184
14796
  flex-wrap: wrap;
14185
14797
  flex-shrink: 0;
14186
14798
  }
14187
- .concierge-persona {
14188
- display: flex; align-items: baseline; gap: 8px;
14189
- font-size: 14px;
14799
+ .concierge-persona { display: flex; align-items: center; gap: 10px; }
14800
+ .concierge-persona .glyph-ring {
14801
+ width: 26px; height: 26px;
14802
+ border: 1.5px solid var(--ink-2);
14803
+ border-radius: 50%;
14804
+ position: relative;
14805
+ flex-shrink: 0;
14806
+ }
14807
+ .concierge-persona .glyph-ring::after {
14808
+ content: ""; position: absolute; inset: 5px;
14809
+ border-radius: 50%; background: var(--ink-2);
14810
+ }
14811
+ .concierge-persona-text { display: flex; flex-direction: column; }
14812
+ .concierge-persona-text strong {
14813
+ font-family: var(--serif); font-weight: 500;
14814
+ font-size: 15px; letter-spacing: 0.005em;
14815
+ }
14816
+ .concierge-persona-text small {
14817
+ color: var(--ink-3); font-size: var(--text-xs);
14818
+ font-family: var(--mono);
14819
+ }
14820
+ .concierge-meta {
14821
+ display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
14190
14822
  }
14191
- .concierge-persona strong { font-size: 14px; }
14192
14823
  .concierge-badge { white-space: nowrap; }
14193
14824
  .concierge-history {
14194
14825
  flex: 1 1 auto;
14195
14826
  min-height: 0;
14196
14827
  overflow-y: auto;
14197
- padding: 14px 4px;
14198
- display: flex; flex-direction: column; gap: 14px;
14828
+ padding: 18px 4px 6px;
14829
+ display: flex; flex-direction: column; gap: 18px;
14199
14830
  }
14200
14831
  .concierge-msg {
14201
- display: flex; flex-direction: column; gap: 4px;
14202
- max-width: 80%;
14832
+ display: flex; flex-direction: column; gap: 5px;
14833
+ max-width: 78%;
14203
14834
  }
14204
14835
  .concierge-msg-author {
14205
- font-size: 11px;
14836
+ font-size: 10px;
14837
+ font-family: var(--mono);
14838
+ letter-spacing: 0.04em;
14839
+ text-transform: uppercase;
14840
+ color: var(--ink-4);
14206
14841
  }
14207
14842
  .concierge-msg-body {
14208
- padding: 10px 14px; border-radius: 12px;
14209
- border: 1px solid var(--rule); background: var(--surface);
14210
- white-space: pre-wrap; word-wrap: break-word;
14211
- font-size: 14px; line-height: 1.5;
14843
+ padding: 11px 14px;
14844
+ border-radius: 12px;
14845
+ border: 1px solid var(--rule);
14846
+ background: var(--surface);
14847
+ font-size: var(--text-md);
14848
+ line-height: 1.55;
14849
+ white-space: pre-wrap;
14850
+ word-wrap: break-word;
14212
14851
  }
14213
14852
  .concierge-msg-concierge { align-self: flex-start; }
14853
+ .concierge-msg-concierge .concierge-msg-body {
14854
+ background: var(--paper-2);
14855
+ }
14214
14856
  .concierge-msg-operator { align-self: flex-end; align-items: flex-end; }
14215
14857
  .concierge-msg-operator .concierge-msg-body {
14216
14858
  background: var(--ink); color: var(--paper); border-color: var(--ink);
14217
14859
  }
14860
+ .concierge-msg-meta {
14861
+ display: flex; gap: 6px;
14862
+ font-size: 10px;
14863
+ color: var(--ink-4);
14864
+ font-family: var(--mono);
14865
+ }
14218
14866
  .concierge-empty {
14219
- padding: 24px 8px; text-align: left;
14220
- font-size: 13px; line-height: 1.6;
14867
+ flex: 1 1 auto;
14868
+ display: flex; flex-direction: column; gap: 22px;
14869
+ justify-content: center;
14870
+ padding: 24px 12px;
14871
+ }
14872
+ .concierge-empty-headline { max-width: 52ch; }
14873
+ .concierge-empty-headline h2 {
14874
+ font-family: var(--serif); font-weight: 400;
14875
+ font-size: var(--text-xl); margin: 0 0 6px;
14876
+ letter-spacing: -0.005em;
14877
+ }
14878
+ .concierge-empty-headline p {
14879
+ color: var(--ink-3); margin: 0;
14880
+ font-size: var(--text-md); line-height: 1.55;
14881
+ }
14882
+ .concierge-suggest-grid {
14883
+ display: grid;
14884
+ grid-template-columns: repeat(3, minmax(0, 1fr));
14885
+ gap: 10px;
14886
+ }
14887
+ .concierge-suggest {
14888
+ background: var(--surface);
14889
+ border: 1px solid var(--rule);
14890
+ border-radius: var(--rad);
14891
+ padding: 12px 14px;
14892
+ font-size: var(--text-base);
14893
+ cursor: pointer;
14894
+ display: flex; flex-direction: column;
14895
+ gap: 6px;
14896
+ text-align: left;
14897
+ font-family: var(--sans);
14898
+ color: var(--ink);
14899
+ }
14900
+ .concierge-suggest:hover:not(:disabled) {
14901
+ background: var(--surface-2);
14902
+ border-color: var(--rule-2);
14903
+ }
14904
+ .concierge-suggest:disabled {
14905
+ cursor: not-allowed; color: var(--ink-4); opacity: 0.7;
14906
+ }
14907
+ .concierge-suggest .label {
14908
+ font-family: var(--mono);
14909
+ font-size: 10px;
14910
+ letter-spacing: 0.06em;
14911
+ text-transform: uppercase;
14912
+ color: var(--ink-3);
14221
14913
  }
14222
14914
  .concierge-composer {
14223
14915
  display: flex; gap: 10px; align-items: center;
14224
- padding: 12px 0 8px;
14916
+ padding: 12px 0 4px;
14225
14917
  border-top: 1px solid var(--rule);
14226
14918
  flex-shrink: 0;
14227
14919
  }
14920
+ .concierge-composer .input-wrap {
14921
+ flex: 1;
14922
+ display: flex; align-items: center; gap: var(--space-2);
14923
+ padding: 8px 12px;
14924
+ border: 1px solid var(--rule);
14925
+ border-radius: var(--rad);
14926
+ background: var(--surface);
14927
+ }
14928
+ .concierge-composer .input-wrap:focus-within {
14929
+ border-color: var(--ink-3);
14930
+ }
14228
14931
  .concierge-composer input {
14229
14932
  flex: 1; min-width: 0;
14230
- padding: 10px 14px;
14231
- border: 1px solid var(--rule); border-radius: var(--rad);
14232
- font-family: var(--sans); font-size: 14px;
14233
- background: var(--surface); color: var(--ink);
14933
+ padding: 4px 0;
14934
+ border: 0;
14935
+ background: transparent;
14936
+ color: var(--ink);
14937
+ font-family: var(--sans);
14938
+ font-size: var(--text-md);
14939
+ outline: none;
14234
14940
  }
14235
- .concierge-composer input:focus {
14236
- outline: none; border-color: var(--ink-3);
14941
+ .concierge-composer input::placeholder { color: var(--ink-4); }
14942
+ .concierge-composer .composer-meta {
14943
+ font-family: var(--mono);
14944
+ font-size: 10px;
14945
+ color: var(--ink-4);
14946
+ letter-spacing: 0.04em;
14237
14947
  }
14238
14948
  .concierge-composer .btn-primary {
14239
- padding: 8px 18px; font-size: 13px; flex-shrink: 0;
14240
- }
14241
- .concierge-chips {
14242
- display: flex; flex-wrap: wrap; gap: 6px;
14243
- padding: 10px 0 0;
14244
- }
14245
- .concierge-chips::before {
14246
- content: "Try:"; color: var(--ink-3); font-size: 12px;
14247
- align-self: center; margin-right: 4px;
14949
+ padding: 8px 18px; font-size: var(--text-base); flex-shrink: 0;
14248
14950
  }
14249
14951
  .concierge-foot {
14250
14952
  margin: 12px 0 0; padding-top: 10px; border-top: 1px dashed var(--rule);
14251
- font-size: 12px;
14953
+ font-size: var(--text-sm);
14252
14954
  }
14253
14955
  .concierge-foot a { color: var(--ink-2); }
14254
14956
  .tier1-approval-card {
@@ -14256,20 +14958,532 @@ body {
14256
14958
  border-radius: var(--rad); padding: 14px 16px; margin: 12px 0;
14257
14959
  }
14258
14960
  .tier1-approval-card h3 {
14259
- margin: 0 0 8px; color: var(--ochre); font-size: 14px;
14961
+ margin: 0 0 8px; color: var(--ochre); font-size: var(--text-md);
14260
14962
  }
14261
- .tier1-approval-card p { margin: 0 0 12px; font-size: 13px; }
14963
+ .tier1-approval-card p { margin: 0 0 12px; font-size: var(--text-base); }
14262
14964
  .tier1-approval-card .actions {
14263
- display: flex; gap: 8px; flex-wrap: wrap;
14965
+ display: flex; gap: var(--space-2); flex-wrap: wrap;
14966
+ }
14967
+ /* Sprint Piece 2 PR 4 (2026-05-04): Agents view + Inspect pane polish.
14968
+ * Translates Claude Design references at
14969
+ * server/docs/design-refs/sprint-piece-2/surface-agents.jsx and the
14970
+ * Surface 3 block of surfaces.css. The fortress-column .agent-row,
14971
+ * .agent-row-head, and .agent-row-actions rules above are kept verbatim
14972
+ * because Finding DD tests pin them; the new Agents-view list scopes
14973
+ * its grid layout under .agents-list (descendant selector) so the
14974
+ * fortress-column rules are unaffected. The inspect pane combines the
14975
+ * existing .card surface with .inspect-pane structure (sticky right
14976
+ * rail, internal scroll, sectioned body) for the agent-detail view.
14977
+ */
14978
+ .agents-wrap { max-width: 1080px; margin: 0 auto; }
14979
+ .agents-layout {
14980
+ display: grid;
14981
+ grid-template-columns: 1fr 420px;
14982
+ gap: 20px;
14983
+ align-items: start;
14984
+ }
14985
+ .agents-list {
14986
+ background: var(--surface);
14987
+ border: 1px solid var(--rule);
14988
+ border-radius: var(--rad-lg);
14989
+ overflow: hidden;
14990
+ }
14991
+ .agents-list-head {
14992
+ display: grid;
14993
+ grid-template-columns: minmax(0, 1fr) 110px 120px 88px;
14994
+ gap: 12px;
14995
+ padding: 10px 16px;
14996
+ border-bottom: 1px solid var(--rule);
14997
+ background: var(--paper-2);
14998
+ font-family: var(--mono);
14999
+ font-size: 10px;
15000
+ letter-spacing: 0.06em;
15001
+ text-transform: uppercase;
15002
+ color: var(--ink-3);
15003
+ }
15004
+ .agents-list .agent-row {
15005
+ display: grid;
15006
+ grid-template-columns: minmax(0, 1fr) 110px 120px 88px;
15007
+ gap: 12px;
15008
+ padding: 14px 16px;
15009
+ border-bottom: 1px solid var(--rule);
15010
+ align-items: center;
15011
+ cursor: pointer;
15012
+ transition: background 120ms ease;
15013
+ }
15014
+ .agents-list .agent-row:last-child { border-bottom: 0; }
15015
+ .agents-list .agent-row:hover { background: var(--paper-2); }
15016
+ .agents-list .agent-row.selected {
15017
+ background: var(--paper-2);
15018
+ box-shadow: inset 3px 0 0 var(--ink);
15019
+ }
15020
+ .agent-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
15021
+ .agent-glyph {
15022
+ width: 28px; height: 28px;
15023
+ border-radius: var(--rad);
15024
+ background: var(--paper-3);
15025
+ border: 1px solid var(--rule);
15026
+ display: grid; place-items: center;
15027
+ flex-shrink: 0;
15028
+ font-family: var(--mono);
15029
+ font-size: 11px;
15030
+ color: var(--ink-2);
15031
+ font-weight: 600;
15032
+ }
15033
+ .agent-name {
15034
+ display: flex; flex-direction: column; min-width: 0;
15035
+ }
15036
+ .agent-name strong {
15037
+ font-size: var(--text-base); font-weight: 500;
15038
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
15039
+ }
15040
+ .agent-name small {
15041
+ font-family: var(--mono); font-size: var(--text-xs); color: var(--ink-3);
15042
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
15043
+ display: block;
15044
+ }
15045
+ .agent-state {
15046
+ display: inline-flex; align-items: center; gap: 6px;
15047
+ font-size: var(--text-xs);
15048
+ font-family: var(--mono);
15049
+ }
15050
+ .state-dot {
15051
+ width: 7px; height: 7px; border-radius: 50%;
15052
+ background: var(--ink-4);
15053
+ }
15054
+ .state-dot.live { background: var(--sage); animation: pulse-soft 2.4s ease-in-out infinite; }
15055
+ .state-dot.idle { background: var(--ochre); }
15056
+ .state-dot.off { background: var(--ink-4); }
15057
+ @keyframes pulse-soft {
15058
+ 0%, 100% { box-shadow: 0 0 0 0 currentColor; opacity: 1; }
15059
+ 50% { box-shadow: 0 0 0 4px transparent; opacity: 0.7; }
15060
+ }
15061
+ .agent-last {
15062
+ font-family: var(--mono); font-size: var(--text-xs); color: var(--ink-3);
15063
+ }
15064
+ /* Inspect pane (combined with .card outer wrapper for the
15065
+ * renderAgentInspectPanel return-shape regex anchored in
15066
+ * dashboard-welcome.test.ts:152). The .inspect-pane modifier overrides
15067
+ * .card padding so internal sections control their own spacing.
15068
+ */
15069
+ .inspect-pane {
15070
+ padding: 0;
15071
+ display: flex; flex-direction: column;
15072
+ position: sticky;
15073
+ top: 20px;
15074
+ max-height: calc(100vh - 100px);
15075
+ overflow: hidden;
15076
+ }
15077
+ .inspect-head {
15078
+ padding: 16px 18px;
15079
+ border-bottom: 1px solid var(--rule);
15080
+ display: flex; flex-direction: column; gap: 10px;
15081
+ }
15082
+ .inspect-head .row1 {
15083
+ display: flex; align-items: center; gap: 10px;
15084
+ }
15085
+ .inspect-head h3 {
15086
+ font-family: var(--serif); font-weight: 500;
15087
+ font-size: 17px; margin: 0;
15088
+ }
15089
+ .inspect-head .meta {
15090
+ display: flex; gap: 6px; flex-wrap: wrap;
15091
+ }
15092
+ .inspect-body {
15093
+ overflow-y: auto;
15094
+ padding: 4px 18px 18px;
15095
+ }
15096
+ .inspect-section {
15097
+ padding: 14px 0;
15098
+ border-bottom: 1px solid var(--rule);
15099
+ }
15100
+ .inspect-section:last-child { border-bottom: 0; }
15101
+ .inspect-section h4 {
15102
+ font-family: var(--mono);
15103
+ font-size: 10px;
15104
+ letter-spacing: 0.08em;
15105
+ text-transform: uppercase;
15106
+ color: var(--ink-3);
15107
+ margin: 0 0 10px;
15108
+ display: flex; align-items: center; justify-content: space-between;
15109
+ }
15110
+ .inspect-section h4 .count {
15111
+ font-family: var(--mono);
15112
+ background: var(--paper-3);
15113
+ border-radius: 999px;
15114
+ padding: 1px 7px;
15115
+ color: var(--ink-2);
15116
+ font-size: 10px;
15117
+ }
15118
+ .approval-row {
15119
+ background: var(--ochre-bg);
15120
+ border: 1px solid var(--ochre);
15121
+ border-radius: var(--rad);
15122
+ padding: 10px 12px;
15123
+ margin-bottom: 8px;
15124
+ display: flex; flex-direction: column; gap: 8px;
15125
+ }
15126
+ .approval-row .what { font-size: var(--text-base); color: var(--ink); }
15127
+ .approval-row .what .pill { margin-right: 6px; }
15128
+ .approval-row .why {
15129
+ font-size: var(--text-sm); color: var(--ink-2);
15130
+ padding-left: 10px;
15131
+ border-left: 2px solid var(--ochre);
15132
+ }
15133
+ .approval-row .actions { display: flex; gap: 6px; justify-content: flex-end; }
15134
+ .timeline {
15135
+ display: flex; flex-direction: column; gap: 0;
15136
+ position: relative;
15137
+ padding-left: 14px;
15138
+ }
15139
+ .timeline::before {
15140
+ content: "";
15141
+ position: absolute;
15142
+ left: 4px; top: 6px; bottom: 6px;
15143
+ width: 1px;
15144
+ background: var(--rule);
15145
+ }
15146
+ .timeline-item {
15147
+ position: relative;
15148
+ padding: 6px 0 10px;
15149
+ font-size: var(--text-sm);
15150
+ }
15151
+ .timeline-item::before {
15152
+ content: "";
15153
+ position: absolute;
15154
+ left: -14px; top: 11px;
15155
+ width: 8px; height: 8px;
15156
+ border-radius: 50%;
15157
+ background: var(--surface);
15158
+ border: 1.5px solid var(--ink-4);
15159
+ }
15160
+ .timeline-item.ok::before { border-color: var(--sage); }
15161
+ .timeline-item.warn::before { border-color: var(--ochre); }
15162
+ .timeline-item.fail::before { border-color: var(--rust); }
15163
+ .timeline-item .ts {
15164
+ font-family: var(--mono); font-size: 10px;
15165
+ color: var(--ink-3);
15166
+ letter-spacing: 0.02em;
15167
+ }
15168
+ .timeline-item .what {
15169
+ margin-top: 2px; color: var(--ink); font-size: var(--text-base);
15170
+ }
15171
+ .timeline-item .att {
15172
+ margin-top: 4px;
15173
+ display: inline-flex;
15174
+ }
15175
+ .policy-line {
15176
+ display: flex; justify-content: space-between; align-items: center;
15177
+ padding: 5px 0; font-size: var(--text-base);
15178
+ border-bottom: 1px dashed var(--rule);
15179
+ }
15180
+ .policy-line:last-child { border-bottom: 0; }
15181
+ .policy-line .k { color: var(--ink-3); }
15182
+ .policy-line .v { font-family: var(--mono); font-size: var(--text-sm); color: var(--ink); }
15183
+ /* Empty-state block for when no agents are wrapped. The
15184
+ * renderAgentsList empty-state branch begins with the literal
15185
+ * '<h1>Agents</h1>' (regex-pinned in agents-empty-state-canary.test.ts)
15186
+ * and the "No wrapped agents yet." copy is preserved verbatim.
15187
+ */
15188
+ .agents-empty {
15189
+ background: var(--surface);
15190
+ border: 1px dashed var(--rule-2);
15191
+ border-radius: var(--rad-lg);
15192
+ padding: 56px 40px;
15193
+ text-align: center;
15194
+ max-width: 720px;
15195
+ margin: 32px auto;
15196
+ }
15197
+ .agents-empty .icon-frame {
15198
+ width: 64px; height: 64px;
15199
+ margin: 0 auto 18px;
15200
+ border: 1px solid var(--rule);
15201
+ border-radius: 50%;
15202
+ display: grid; place-items: center;
15203
+ position: relative;
15204
+ }
15205
+ .agents-empty .icon-frame::before,
15206
+ .agents-empty .icon-frame::after {
15207
+ content: "";
15208
+ position: absolute;
15209
+ border: 1px solid var(--rule);
15210
+ border-radius: 50%;
15211
+ }
15212
+ .agents-empty .icon-frame::before { inset: -8px; opacity: 0.6; }
15213
+ .agents-empty .icon-frame::after { inset: -16px; opacity: 0.3; }
15214
+ .agents-empty .icon-frame .core {
15215
+ width: 22px; height: 22px;
15216
+ background: var(--ink);
15217
+ border-radius: 50%;
15218
+ }
15219
+ .agents-empty h2 {
15220
+ font-family: var(--serif);
15221
+ font-weight: 400;
15222
+ font-size: var(--text-xl);
15223
+ margin: 0 0 8px;
15224
+ }
15225
+ .agents-empty p {
15226
+ color: var(--ink-3);
15227
+ margin: 0 0 20px;
15228
+ font-size: var(--text-md);
15229
+ line-height: 1.55;
15230
+ max-width: 50ch;
15231
+ margin-left: auto; margin-right: auto;
15232
+ }
15233
+ .terminal-block {
15234
+ text-align: left;
15235
+ background: var(--paper-3);
15236
+ border: 1px solid var(--rule);
15237
+ border-radius: var(--rad);
15238
+ padding: 14px 16px;
15239
+ font-family: var(--mono);
15240
+ font-size: var(--text-base);
15241
+ margin: 0 auto 16px;
15242
+ max-width: 480px;
15243
+ display: flex; align-items: center; justify-content: space-between;
15244
+ }
15245
+ .terminal-block .cmd { color: var(--ink); }
15246
+ .terminal-block .cmd .prompt { color: var(--ink-3); margin-right: 8px; user-select: none; }
15247
+ .copy-btn {
15248
+ background: transparent; border: 0;
15249
+ color: var(--ink-3); cursor: pointer;
15250
+ font-family: var(--mono); font-size: var(--text-xs);
15251
+ padding: 2px 6px;
15252
+ border-radius: var(--rad);
15253
+ }
15254
+ .copy-btn:hover { color: var(--ink); background: var(--paper-2); }
15255
+
15256
+ /* Surface 5. Attestation badge gallery. */
15257
+ .att-gallery {
15258
+ display: flex; flex-direction: column; gap: 24px;
15259
+ max-width: 1000px;
15260
+ margin: 0 auto;
14264
15261
  }
15262
+ .att-section {
15263
+ background: var(--surface);
15264
+ border: 1px solid var(--rule);
15265
+ border-radius: var(--rad-lg);
15266
+ padding: 22px 24px;
15267
+ }
15268
+ .att-section-head {
15269
+ display: flex; justify-content: space-between; align-items: baseline;
15270
+ gap: 12px;
15271
+ margin-bottom: 16px;
15272
+ padding-bottom: 12px;
15273
+ border-bottom: 1px solid var(--rule);
15274
+ }
15275
+ .att-section-head h2 {
15276
+ font-family: var(--serif);
15277
+ font-weight: 400;
15278
+ font-size: 19px;
15279
+ margin: 0 0 4px;
15280
+ }
15281
+ .att-section-head p {
15282
+ color: var(--ink-3);
15283
+ margin: 0;
15284
+ font-size: 13px;
15285
+ line-height: 1.5;
15286
+ max-width: 64ch;
15287
+ }
15288
+ .att-section-head .label {
15289
+ font-family: var(--mono);
15290
+ font-size: 10px;
15291
+ letter-spacing: 0.08em;
15292
+ text-transform: uppercase;
15293
+ color: var(--ink-3);
15294
+ }
15295
+ .att-row {
15296
+ display: grid;
15297
+ grid-template-columns: 240px 1fr;
15298
+ gap: 24px;
15299
+ padding: 14px 0;
15300
+ border-bottom: 1px dashed var(--rule);
15301
+ align-items: center;
15302
+ }
15303
+ .att-row:last-child { border-bottom: 0; }
15304
+ .att-row .demo {
15305
+ display: flex; align-items: center; justify-content: flex-start;
15306
+ padding: 12px 16px;
15307
+ background: var(--paper-2);
15308
+ border: 1px solid var(--rule);
15309
+ border-radius: var(--rad);
15310
+ min-height: 56px;
15311
+ }
15312
+ .att-row .desc strong {
15313
+ font-size: 13px; display: block; margin-bottom: 3px;
15314
+ }
15315
+ .att-row .desc small {
15316
+ color: var(--ink-3); font-size: 12px;
15317
+ line-height: 1.5;
15318
+ }
15319
+
15320
+ /* Global persistent badge. Lives in the topbar across every surface. */
15321
+ .att-global {
15322
+ display: inline-flex; align-items: center;
15323
+ gap: 8px;
15324
+ padding: 4px 10px 4px 6px;
15325
+ border: 1px solid var(--rule);
15326
+ border-radius: 999px;
15327
+ background: var(--surface-2);
15328
+ font-family: var(--mono);
15329
+ font-size: 11px;
15330
+ color: var(--ink-2);
15331
+ }
15332
+ .att-global.verified { border-color: var(--sage); background: var(--sage-bg); color: var(--sage); }
15333
+ .att-global.degraded { border-color: var(--ochre); background: var(--ochre-bg); color: var(--ochre); }
15334
+ .att-global.unverified { border-color: var(--rust); background: var(--rust-bg); color: var(--rust); }
15335
+ .att-global .seal {
15336
+ width: 18px; height: 18px;
15337
+ position: relative;
15338
+ flex-shrink: 0;
15339
+ }
15340
+ .att-global .seal-ring {
15341
+ position: absolute; inset: 0;
15342
+ border: 1.5px solid currentColor;
15343
+ border-radius: 50%;
15344
+ }
15345
+ .att-global .seal-ring.dashed { border-style: dashed; }
15346
+ .att-global .seal-core {
15347
+ position: absolute; inset: 4px;
15348
+ background: currentColor;
15349
+ border-radius: 50%;
15350
+ opacity: 0.85;
15351
+ }
15352
+ .att-global.degraded .seal-core { background: transparent; border: 1px solid currentColor; }
15353
+ .att-global.unverified .seal-core {
15354
+ background: transparent;
15355
+ border: 1px solid currentColor;
15356
+ }
15357
+ .att-global.unverified .seal-core::after {
15358
+ content: ""; position: absolute; inset: 0;
15359
+ background: currentColor; opacity: 0.4;
15360
+ clip-path: polygon(0 0, 100% 100%, 100% 90%, 10% 0);
15361
+ }
15362
+ .att-global .label {
15363
+ font-family: var(--mono);
15364
+ font-size: 11px;
15365
+ letter-spacing: 0.02em;
15366
+ text-transform: uppercase;
15367
+ }
15368
+ .att-global .hash {
15369
+ font-family: var(--mono);
15370
+ font-size: 10px;
15371
+ opacity: 0.7;
15372
+ border-left: 1px solid currentColor;
15373
+ padding-left: 8px;
15374
+ margin-left: 2px;
15375
+ }
15376
+
15377
+ /* Per-agent badge. Square chip beside each agent. */
15378
+ .att-agent {
15379
+ display: inline-flex; align-items: center;
15380
+ gap: 6px;
15381
+ padding: 3px 7px;
15382
+ border-radius: var(--rad);
15383
+ border: 1px solid var(--rule);
15384
+ background: var(--surface);
15385
+ font-family: var(--mono);
15386
+ font-size: 10px;
15387
+ color: var(--ink-2);
15388
+ }
15389
+ .att-agent .mark {
15390
+ width: 10px; height: 10px;
15391
+ border: 1.5px solid currentColor;
15392
+ border-radius: 2px;
15393
+ position: relative;
15394
+ }
15395
+ .att-agent.verified { color: var(--sage); border-color: var(--sage); background: var(--sage-bg); }
15396
+ .att-agent.verified .mark { background: currentColor; }
15397
+ .att-agent.degraded { color: var(--ochre); border-color: var(--ochre); background: var(--ochre-bg); }
15398
+ .att-agent.unverified { color: var(--rust); border-color: var(--rust); background: var(--rust-bg); }
15399
+ .att-agent.unverified .mark {
15400
+ background: repeating-linear-gradient(
15401
+ 45deg, currentColor, currentColor 1px,
15402
+ transparent 1px, transparent 3px
15403
+ );
15404
+ }
15405
+
15406
+ /* Per-action badge. Tiny inline tick on timeline rows. */
15407
+ .att-action {
15408
+ display: inline-flex; align-items: center; gap: 4px;
15409
+ font-family: var(--mono);
15410
+ font-size: 10px;
15411
+ color: var(--ink-3);
15412
+ padding: 1px 6px;
15413
+ border-radius: 4px;
15414
+ background: var(--paper-3);
15415
+ border: 1px solid transparent;
15416
+ }
15417
+ .att-action .tick {
15418
+ width: 6px; height: 6px;
15419
+ border-radius: 1px;
15420
+ background: currentColor;
15421
+ }
15422
+ .att-action.verified { color: var(--sage); }
15423
+ .att-action.degraded { color: var(--ochre); }
15424
+ .att-action.unverified { color: var(--rust); }
15425
+ .att-action.neutral .tick { background: var(--ink-4); border-radius: 50%; }
15426
+
15427
+ /* Custody-provenance badge stub (v1.x). Visibly stubbed with dashed border. */
15428
+ .att-custody {
15429
+ display: inline-flex; align-items: center; gap: 8px;
15430
+ padding: 4px 10px 4px 6px;
15431
+ border-radius: var(--rad);
15432
+ border: 1px dashed var(--rule-2);
15433
+ background: var(--paper-3);
15434
+ color: var(--ink-3);
15435
+ font-family: var(--mono);
15436
+ font-size: 10px;
15437
+ }
15438
+ .att-custody .seal-stub {
15439
+ width: 16px; height: 16px;
15440
+ border: 1px dashed var(--ink-4);
15441
+ border-radius: 50%;
15442
+ position: relative;
15443
+ flex-shrink: 0;
15444
+ }
15445
+ .att-custody .seal-stub::after {
15446
+ content: ""; position: absolute; inset: 4px;
15447
+ border: 1px dashed var(--ink-4);
15448
+ border-radius: 50%;
15449
+ }
15450
+ .att-custody .stub-tag {
15451
+ letter-spacing: 0.06em;
15452
+ text-transform: uppercase;
15453
+ }
15454
+
15455
+ /* Tooltip surface for badges. */
15456
+ .att-tooltip {
15457
+ background: var(--ink);
15458
+ color: var(--paper);
15459
+ font-family: var(--mono);
15460
+ font-size: 11px;
15461
+ padding: 8px 10px;
15462
+ border-radius: var(--rad);
15463
+ max-width: 280px;
15464
+ line-height: 1.5;
15465
+ display: inline-block;
15466
+ }
15467
+ [data-theme="dark"] .att-tooltip {
15468
+ background: var(--paper-3);
15469
+ color: var(--ink);
15470
+ }
15471
+
14265
15472
  @media (max-width: 1100px) {
14266
15473
  .app, .app.route-full { grid-template-columns: 56px 1fr; grid-template-areas: "sidebar topbar" "sidebar main"; }
14267
15474
  .fortress { display: none; }
14268
15475
  .sidebar h1, .sidebar nav a span { display: none; }
15476
+ .sidebar nav a { justify-content: center; padding: 8px 6px; }
14269
15477
  .template-grid { grid-template-columns: 1fr; }
14270
15478
  .policy-center h1 { font-size: 30px; }
14271
15479
  .intel-center h1 { font-size: 30px; }
14272
15480
  .intel-row { grid-template-columns: 1fr; }
15481
+ .intel-grid { grid-template-columns: 1fr; }
15482
+ .intel-failure-row { grid-template-columns: 1fr; }
15483
+ .agents-layout { grid-template-columns: 1fr; }
15484
+ .agents-list-head, .agents-list .agent-row { grid-template-columns: minmax(0, 1fr) 90px 90px; }
15485
+ .agents-list-head span:nth-child(4), .agents-list .agent-row > .agent-last { display: none; }
15486
+ .inspect-pane { position: static; max-height: none; }
14273
15487
  }
14274
15488
  `;
14275
15489
  var NAV_ITEMS = [
@@ -14277,11 +15491,26 @@ var NAV_ITEMS = [
14277
15491
  { id: "agents", label: "Agents" },
14278
15492
  { id: "policy", label: "Policy" },
14279
15493
  { id: "intelligence", label: "Intelligence" },
15494
+ { id: "attestation", label: "Attestation" },
14280
15495
  { id: "privacy", label: "Privacy" },
14281
15496
  { id: "coordination", label: "Coordination" },
14282
15497
  { id: "health", label: "Health" },
14283
15498
  { id: "exit-drill", label: "Exit drill" }
14284
15499
  ];
15500
+ var NAV_ICON_PATHS = {
15501
+ dashboard: '<rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/>',
15502
+ agents: '<circle cx="6" cy="5.5" r="2.3"/><path d="M2 13.5c0-2.2 1.8-4 4-4s4 1.8 4 4"/><circle cx="11.5" cy="6" r="1.8"/><path d="M14 12.5c0-1.7-1.1-2.7-2.5-3"/>',
15503
+ policy: '<path d="M4 2h5l3 3v9H4z"/><path d="M9 2v3h3"/><path d="M6 9h4M6 11.5h4"/>',
15504
+ intelligence: '<rect x="3.5" y="3.5" width="9" height="9" rx="0.5"/><rect x="6" y="6" width="4" height="4"/><path d="M6 1.5v2M10 1.5v2M6 12.5v2M10 12.5v2M1.5 6h2M1.5 10h2M12.5 6h2M12.5 10h2"/>',
15505
+ attestation: '<circle cx="8" cy="8" r="5.5"/><circle cx="8" cy="8" r="2.2"/><path d="M8 1.5v1.5M8 13v1.5M1.5 8h1.5M13 8h1.5"/>',
15506
+ privacy: '<path d="M8 1.5L3 3v4.5c0 3 2.2 5.4 5 7 2.8-1.6 5-4 5-7V3z"/><path d="M6 8l1.5 1.5L10.5 6"/>',
15507
+ coordination: '<circle cx="4" cy="3.5" r="1.4"/><circle cx="4" cy="12.5" r="1.4"/><circle cx="12" cy="8" r="1.4"/><path d="M4 4.9V11.1M4 5c0 3 3 3 6.7 3"/>',
15508
+ health: '<path d="M2 8h2.5l1.5-4 3 8 1.5-4H14"/>',
15509
+ "exit-drill": '<path d="M9.5 2H3v12h6.5"/><path d="M11 5l3 3-3 3"/><path d="M14 8H6.5"/>'
15510
+ };
15511
+ var SVG_OPEN = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">';
15512
+ var THEME_ICON_MOON = SVG_OPEN + '<path d="M13 9.5A5.5 5.5 0 0 1 6.5 3 5.5 5.5 0 1 0 13 9.5z"/></svg>';
15513
+ var THEME_ICON_SUN = SVG_OPEN + '<circle cx="8" cy="8" r="2.5"/><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.2 3.2l1.4 1.4M11.4 11.4l1.4 1.4M3.2 12.8l1.4-1.4M11.4 4.6l1.4-1.4"/></svg>';
14285
15514
  function escHtml2(value) {
14286
15515
  if (value == null) return "";
14287
15516
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
@@ -14294,9 +15523,11 @@ function renderDashboardV11Html(options = {}) {
14294
15523
  const fortressId = options.fortressId ?? "fortress";
14295
15524
  const sanctuaryVersion = options.sanctuaryVersion ?? SANCTUARY_VERSION;
14296
15525
  const embedClient = options.embedClient !== false;
14297
- const nav = NAV_ITEMS.map(
14298
- (n) => `<a href="#${n.id}" data-route="${n.id}"><span>${escHtml2(n.label)}</span></a>`
14299
- ).join("\n ");
15526
+ const nav = NAV_ITEMS.map((n) => {
15527
+ const iconPath = NAV_ICON_PATHS[n.id] ?? "";
15528
+ const icon = iconPath ? SVG_OPEN + iconPath + "</svg>" : "";
15529
+ return `<a href="#${n.id}" data-route="${n.id}">${icon}<span>${escHtml2(n.label)}</span></a>`;
15530
+ }).join("\n ");
14300
15531
  const config = JSON.stringify({
14301
15532
  authToken,
14302
15533
  hubApiBase,
@@ -14328,8 +15559,12 @@ function renderDashboardV11Html(options = {}) {
14328
15559
  <span class="pill" data-pill="version">v${escHtml2(sanctuaryVersion)}</span>
14329
15560
  <span class="pill" data-pill="deployment">deployment: local</span>
14330
15561
  <span class="pill" data-pill="mode">mode: solo</span>
14331
- <span class="pill" data-pill="attestation">attestation: pending</span>
15562
+ <span class="att-global pending" data-pill="attestation" title="Fortress attestation"><span class="seal"><span class="seal-ring dashed"></span><span class="seal-core"></span></span><span class="label">pending</span></span>
14332
15563
  </div>
15564
+ <button class="btn btn-icon" id="btn-theme-toggle" data-action="theme-toggle" aria-label="Toggle theme" title="Toggle theme">
15565
+ <span class="icon-moon">${THEME_ICON_MOON}</span>
15566
+ <span class="icon-sun">${THEME_ICON_SUN}</span>
15567
+ </button>
14333
15568
  <button class="btn btn-danger" id="btn-lockdown" data-action="lockdown">Lockdown</button>
14334
15569
  </header>
14335
15570
  <main class="main" id="main"><p class="muted">Loading dashboard.</p></main>
@@ -17662,8 +18897,8 @@ function verifySHR(shr, now) {
17662
18897
  const errors = [];
17663
18898
  const warnings = [];
17664
18899
  const currentTime = now ?? /* @__PURE__ */ new Date();
17665
- if (!shr.body || !shr.signed_by || !shr.signature) {
17666
- errors.push("Missing required SHR fields (body, signed_by, or signature)");
18900
+ if (!shr.body || !shr.signed_by || !shr.signature || !shr.signature_scheme) {
18901
+ errors.push("Missing required SHR fields (body, signed_by, signature_scheme, or signature)");
17667
18902
  return {
17668
18903
  valid: false,
17669
18904
  errors,
@@ -17676,6 +18911,9 @@ function verifySHR(shr, now) {
17676
18911
  if (shr.body.shr_version !== "1.0") {
17677
18912
  errors.push(`Unsupported SHR version: ${shr.body.shr_version}`);
17678
18913
  }
18914
+ if (shr.signature_scheme !== SIGNATURE_SCHEME_V1) {
18915
+ errors.push(`Unsupported SHR signature_scheme: ${String(shr.signature_scheme)}`);
18916
+ }
17679
18917
  const expiresAt = new Date(shr.body.expires_at);
17680
18918
  if (isNaN(expiresAt.getTime())) {
17681
18919
  errors.push("Invalid expires_at timestamp");
@@ -28733,8 +29971,6 @@ function aggregateInbox(sources, store) {
28733
29971
  }
28734
29972
  return store.list();
28735
29973
  }
28736
-
28737
- // src/hub/activity-feed.ts
28738
29974
  var LIFECYCLE_VERBS = [
28739
29975
  "wrap",
28740
29976
  "unwrap",
@@ -28793,18 +30029,30 @@ function extractAgentIdHint(entry) {
28793
30029
  const value = details.agent_id;
28794
30030
  return typeof value === "string" && value.length > 0 ? value : void 0;
28795
30031
  }
30032
+ function deriveAttestationFragment(entryId) {
30033
+ const hash2 = createHash("sha256").update(entryId).digest("hex");
30034
+ return `${hash2.slice(0, 4)}..${hash2.slice(4, 6)}`;
30035
+ }
30036
+ function deriveAttestationState(entry) {
30037
+ return entry.result === "success" ? "verified" : "degraded";
30038
+ }
28796
30039
  function projectEntry(entry) {
28797
30040
  const agentIdHint = extractAgentIdHint(entry);
28798
30041
  const category = categorizeOperation(entry.layer, entry.operation);
30042
+ const entryId = `${entry.timestamp}|${entry.operation}|${entry.identity_id}`;
28799
30043
  return {
28800
30044
  version: "1.1",
28801
- entry_id: `${entry.timestamp}|${entry.operation}|${entry.identity_id}`,
30045
+ entry_id: entryId,
28802
30046
  emitted_at: entry.timestamp,
28803
30047
  ...agentIdHint ? { agent_id: agentIdHint } : {},
28804
30048
  identity_id: entry.identity_id,
28805
30049
  category,
28806
30050
  display_template_id: templateIdFor(category, entry.operation),
28807
- display_template_args: buildTemplateArgs(entry, agentIdHint)
30051
+ display_template_args: buildTemplateArgs(entry, agentIdHint),
30052
+ attestation: {
30053
+ state: deriveAttestationState(entry),
30054
+ fragment: deriveAttestationFragment(entryId)
30055
+ }
28808
30056
  };
28809
30057
  }
28810
30058
  async function aggregateActivity(sources, filter = {}) {
@@ -31753,7 +33001,7 @@ var MemoryStorage = class {
31753
33001
  };
31754
33002
 
31755
33003
  // src/contracts/v1.1/constants.ts
31756
- var SIGNATURE_SCHEME_V1 = "ed25519-v1";
33004
+ var SIGNATURE_SCHEME_V12 = "ed25519-v1";
31757
33005
  var EXIT_BUNDLE_MANIFEST_VERSION = "SANCTUARY_EXIT_BUNDLE_V1";
31758
33006
  var EXIT_BUNDLE_ARTIFACT_KINDS = [
31759
33007
  "public_identity",
@@ -31778,6 +33026,12 @@ var EXIT_BUNDLE_PATH_MAX_BYTES = 256;
31778
33026
  // src/exit/verifier.ts
31779
33027
  init_encoding();
31780
33028
  init_hashing();
33029
+ var InvalidExitBundleError = class extends Error {
33030
+ constructor(message) {
33031
+ super(message);
33032
+ this.name = "InvalidExitBundleError";
33033
+ }
33034
+ };
31781
33035
  var PRIVATE_MATERIAL_KEYS = /* @__PURE__ */ new Set([
31782
33036
  "private_key",
31783
33037
  "privatekey",
@@ -31960,7 +33214,7 @@ function verifyReputationArtifact(reputationArtifact, publicKeysByDid) {
31960
33214
  unverifiable_attestations: unverifiable
31961
33215
  };
31962
33216
  }
31963
- async function verifyExitBundle(bundleDir) {
33217
+ async function verifyExitBundle(bundleDir, options = {}) {
31964
33218
  const root = resolve(bundleDir);
31965
33219
  let manifest;
31966
33220
  let manifestBytes;
@@ -31969,7 +33223,9 @@ async function verifyExitBundle(bundleDir) {
31969
33223
  manifestBytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
31970
33224
  manifest = JSON.parse(Buffer.from(raw).toString("utf8"));
31971
33225
  } catch {
31972
- return fail(root, null, "other", ["manifest.json is missing or unreadable"]);
33226
+ throw new InvalidExitBundleError(
33227
+ `Not a valid SANCTUARY_EXIT_BUNDLE_V1 directory: manifest.json missing at ${join(root, "manifest.json")}`
33228
+ );
31973
33229
  }
31974
33230
  const warnings = [];
31975
33231
  const unsupportedArtifacts = [];
@@ -31977,7 +33233,7 @@ async function verifyExitBundle(bundleDir) {
31977
33233
  if (!body || body.manifest_version !== EXIT_BUNDLE_MANIFEST_VERSION) {
31978
33234
  return fail(root, manifest, "manifest_unknown_version", warnings, unsupportedArtifacts);
31979
33235
  }
31980
- if (body.signature_scheme !== SIGNATURE_SCHEME_V1) {
33236
+ if (body.signature_scheme !== SIGNATURE_SCHEME_V12) {
31981
33237
  return fail(
31982
33238
  root,
31983
33239
  manifest,
@@ -32137,9 +33393,16 @@ async function verifyExitBundle(bundleDir) {
32137
33393
  }
32138
33394
  const reputationFailed = reputation?.bundle_signature_valid === false || (reputation?.invalid_attestations ?? 0) > 0;
32139
33395
  const identityFailed = identity ? !identity.signature_valid : false;
33396
+ const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
33397
+ const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
33398
+ if (unverifiableFailed) {
33399
+ warnings.push(
33400
+ `${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
33401
+ );
33402
+ }
32140
33403
  return {
32141
33404
  version: "1.1",
32142
- passed: !reputationFailed && !identityFailed,
33405
+ passed: !reputationFailed && !identityFailed && !unverifiableFailed,
32143
33406
  verified_at: (/* @__PURE__ */ new Date()).toISOString(),
32144
33407
  manifest_path: join(root, "manifest.json"),
32145
33408
  manifest_hash: sha256Hex2(manifestBytes),
@@ -32156,7 +33419,7 @@ async function verifyExitBundle(bundleDir) {
32156
33419
  identity,
32157
33420
  audit,
32158
33421
  reputation,
32159
- failure_class: reputationFailed || identityFailed ? "other" : void 0
33422
+ failure_class: reputationFailed || identityFailed || unverifiableFailed ? "other" : void 0
32160
33423
  };
32161
33424
  }
32162
33425
 
@@ -32169,6 +33432,14 @@ var EXIT_POLICY_SETS_NAMESPACE = "_exit_policy_sets";
32169
33432
  var EXIT_COMMITMENTS_NAMESPACE = "_exit_commitments";
32170
33433
  var EXIT_PLACEHOLDER_METADATA_NAMESPACE = "_exit_placeholder_metadata";
32171
33434
  var PRIVACY_PLACEHOLDER_NAMESPACE = "_privacy_placeholder_vault";
33435
+ var ExitBundleImportError = class extends Error {
33436
+ code;
33437
+ constructor(code, message) {
33438
+ super(message);
33439
+ this.name = "ExitBundleImportError";
33440
+ this.code = code;
33441
+ }
33442
+ };
32172
33443
  function sha256Hex3(bytes) {
32173
33444
  return Array.from(hash(bytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
32174
33445
  }
@@ -32461,7 +33732,7 @@ async function exportExitBundle(opts) {
32461
33732
  ),
32462
33733
  artifacts_aggregate_hash_alg: "sha256",
32463
33734
  export_approval_audit_id: exportApprovalAuditId,
32464
- signature_scheme: SIGNATURE_SCHEME_V1
33735
+ signature_scheme: SIGNATURE_SCHEME_V12
32465
33736
  };
32466
33737
  const signature = sign(
32467
33738
  canonicalizeToBytes(body),
@@ -32646,7 +33917,9 @@ async function stageArtifact(storage, namespace, key, value) {
32646
33917
  await storage.write(namespace, key, jsonBytes(value));
32647
33918
  }
32648
33919
  async function importExitBundle(opts) {
32649
- const verification = await verifyExitBundle(opts.bundleDir);
33920
+ const verification = await verifyExitBundle(opts.bundleDir, {
33921
+ acceptUnverifiableAttestations: opts.acceptUnverifiableAttestations
33922
+ });
32650
33923
  if (!verification.passed) {
32651
33924
  return {
32652
33925
  verified: false,
@@ -32742,6 +34015,23 @@ async function importExitBundle(opts) {
32742
34015
  unsupported_artifacts: verification.unsupported_artifacts
32743
34016
  };
32744
34017
  }
34018
+ if (conflicts.public_identity_exists && !opts.forceRebind) {
34019
+ throw new ExitBundleImportError(
34020
+ "IDENTITY_OVERWRITE_REFUSED",
34021
+ "Importing this bundle would overwrite an existing fortress public identity. Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
34022
+ );
34023
+ }
34024
+ if (conflicts.public_identity_exists && opts.forceRebind && identityArtifact) {
34025
+ opts.auditLog.append(
34026
+ "l1",
34027
+ "exit_bundle_force_rebind",
34028
+ identityArtifact.json.bundle.identity_id,
34029
+ {
34030
+ manifest_version: manifest.body.manifest_version,
34031
+ fortress_id: manifest.body.identity_binding.fortress_id
34032
+ }
34033
+ );
34034
+ }
32745
34035
  const importId = importIdForManifest(manifest);
32746
34036
  const stagedArtifacts = [];
32747
34037
  if (identityArtifact) {
@@ -32852,7 +34142,7 @@ function exitBundleManifestShape() {
32852
34142
  manifest_version: EXIT_BUNDLE_MANIFEST_VERSION,
32853
34143
  artifacts: [...EXIT_BUNDLE_ARTIFACT_KINDS],
32854
34144
  hash_alg: "sha256",
32855
- signature_scheme: SIGNATURE_SCHEME_V1,
34145
+ signature_scheme: SIGNATURE_SCHEME_V12,
32856
34146
  required_top_level_file: "manifest.json",
32857
34147
  artifact_paths: [
32858
34148
  "artifacts/public_identity.json",
@@ -32961,6 +34251,11 @@ Options:
32961
34251
  --destination-identity-id <id> Destination signer for re-keyed state
32962
34252
  --state-namespace <name> Export a namespace; repeatable
32963
34253
  --conflict <skip|overwrite|version>
34254
+ --force-rebind On import: explicitly replace an existing fortress
34255
+ public identity (Tier 1 confirmation)
34256
+ --accept-unverifiable-attestations
34257
+ On import: accept reputation attestations whose
34258
+ signer DID is not in the bundle (Tier 1 confirmation)
32964
34259
  --json
32965
34260
  --yes, -y Explicit non-interactive Tier 1 approval
32966
34261
  --help, -h
@@ -32989,7 +34284,22 @@ async function runExitCommand(args) {
32989
34284
  write(err, "Usage: sanctuary exit verify <dir>\n");
32990
34285
  return 2;
32991
34286
  }
32992
- const result = await verifyExitBundle(dir);
34287
+ let result;
34288
+ try {
34289
+ result = await verifyExitBundle(dir, {
34290
+ acceptUnverifiableAttestations: hasFlag(
34291
+ argv,
34292
+ "--accept-unverifiable-attestations"
34293
+ )
34294
+ });
34295
+ } catch (e) {
34296
+ if (e instanceof InvalidExitBundleError) {
34297
+ write(err, `Error: ${e.message}
34298
+ `);
34299
+ return 1;
34300
+ }
34301
+ throw e;
34302
+ }
32993
34303
  if (json) {
32994
34304
  write(out, JSON.stringify(result, null, 2) + "\n");
32995
34305
  } else {
@@ -33067,9 +34377,15 @@ async function runExitCommand(args) {
33067
34377
  return 2;
33068
34378
  }
33069
34379
  const activate = hasFlag(argv, "--activate");
34380
+ const forceRebind = hasFlag(argv, "--force-rebind");
34381
+ const acceptUnverifiableAttestations = hasFlag(
34382
+ argv,
34383
+ "--accept-unverifiable-attestations"
34384
+ );
33070
34385
  if (activate) {
34386
+ const prompt = forceRebind ? "Tier 1 approval required: activate verified imported exit bundle AND replace the existing fortress public identity (force-rebind)?" : "Tier 1 approval required: activate verified imported exit bundle?";
33071
34387
  const approved = await confirmTier1(
33072
- "Tier 1 approval required: activate verified imported exit bundle?",
34388
+ prompt,
33073
34389
  hasFlag(argv, "--yes") || hasFlag(argv, "-y"),
33074
34390
  stdin,
33075
34391
  err
@@ -33078,6 +34394,18 @@ async function runExitCommand(args) {
33078
34394
  write(err, "Aborted.\n");
33079
34395
  return 1;
33080
34396
  }
34397
+ if (acceptUnverifiableAttestations) {
34398
+ const acceptApproved = await confirmTier1(
34399
+ "Tier 1 approval required: accept unverifiable reputation attestations on import?",
34400
+ hasFlag(argv, "--yes") || hasFlag(argv, "-y"),
34401
+ stdin,
34402
+ err
34403
+ );
34404
+ if (!acceptApproved) {
34405
+ write(err, "Aborted.\n");
34406
+ return 1;
34407
+ }
34408
+ }
33081
34409
  }
33082
34410
  const ctx = await openExitContext(argv, env);
33083
34411
  const conflict = flagValue(argv, "--conflict") ?? "skip";
@@ -33085,19 +34413,31 @@ async function runExitCommand(args) {
33085
34413
  write(err, "--conflict must be skip, overwrite, or version\n");
33086
34414
  return 2;
33087
34415
  }
33088
- const result = await importExitBundle({
33089
- bundleDir: dir,
33090
- storage: ctx.storage,
33091
- masterKey: ctx.masterKey,
33092
- identityManager: ctx.identityManager,
33093
- auditLog: ctx.auditLog,
33094
- reputationStore: ctx.reputationStore,
33095
- activate,
33096
- conflictResolution: conflict,
33097
- sourcePassphrase: flagValue(argv, "--source-passphrase"),
33098
- sourceRecoveryKey: flagValue(argv, "--source-recovery-key"),
33099
- destinationSignerIdentityId: flagValue(argv, "--destination-identity-id")
33100
- });
34416
+ let result;
34417
+ try {
34418
+ result = await importExitBundle({
34419
+ bundleDir: dir,
34420
+ storage: ctx.storage,
34421
+ masterKey: ctx.masterKey,
34422
+ identityManager: ctx.identityManager,
34423
+ auditLog: ctx.auditLog,
34424
+ reputationStore: ctx.reputationStore,
34425
+ activate,
34426
+ forceRebind,
34427
+ acceptUnverifiableAttestations,
34428
+ conflictResolution: conflict,
34429
+ sourcePassphrase: flagValue(argv, "--source-passphrase"),
34430
+ sourceRecoveryKey: flagValue(argv, "--source-recovery-key"),
34431
+ destinationSignerIdentityId: flagValue(argv, "--destination-identity-id")
34432
+ });
34433
+ } catch (e) {
34434
+ if (e instanceof InvalidExitBundleError) {
34435
+ write(err, `Error: ${e.message}
34436
+ `);
34437
+ return 1;
34438
+ }
34439
+ throw e;
34440
+ }
33101
34441
  if (json) write(out, JSON.stringify(result, null, 2) + "\n");
33102
34442
  else {
33103
34443
  write(out, `verified: ${result.verified}
@@ -33327,7 +34667,7 @@ async function createSanctuaryServer(options) {
33327
34667
  const hasKeyParams = existingNamespaces.some((e) => e.key === "key-params");
33328
34668
  if (hasKeyParams) {
33329
34669
  throw new Error(
33330
- "Sanctuary: Found existing key derivation parameters but no recovery key hash.\nThis indicates a corrupted or incomplete installation.\nIf you previously used a passphrase, set SANCTUARY_PASSPHRASE to start."
34670
+ "Sanctuary: passphrase required.\n\nThe fortress at this path uses passphrase-mode key derivation.\nSet SANCTUARY_PASSPHRASE in your environment, or run\n'sanctuary export-passphrase' to retrieve it from the macOS Keychain."
33331
34671
  );
33332
34672
  }
33333
34673
  masterKey = generateRandomKey();
@@ -33908,6 +35248,6 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
33908
35248
  };
33909
35249
  }
33910
35250
 
33911
- export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize2 as canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate2 as getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
35251
+ export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, ExitBundleImportError, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize2 as canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate2 as getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
33912
35252
  //# sourceMappingURL=index.js.map
33913
35253
  //# sourceMappingURL=index.js.map