instar 1.3.1669 → 1.3.1671

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 (125) hide show
  1. package/dist/commands/init.d.ts +6 -3
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +9 -6
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/server.d.ts.map +1 -1
  6. package/dist/commands/server.js +41 -17
  7. package/dist/commands/server.js.map +1 -1
  8. package/dist/core/BackupManager.d.ts.map +1 -1
  9. package/dist/core/BackupManager.js +6 -0
  10. package/dist/core/BackupManager.js.map +1 -1
  11. package/dist/core/ClassReviewReplicatedStore.d.ts +13 -1
  12. package/dist/core/ClassReviewReplicatedStore.d.ts.map +1 -1
  13. package/dist/core/ClassReviewReplicatedStore.js +16 -0
  14. package/dist/core/ClassReviewReplicatedStore.js.map +1 -1
  15. package/dist/core/CodexLifecycleProductionComposition.d.ts +1 -1
  16. package/dist/core/CodexLifecycleProductionComposition.d.ts.map +1 -1
  17. package/dist/core/FileClassifier.d.ts.map +1 -1
  18. package/dist/core/FileClassifier.js +4 -0
  19. package/dist/core/FileClassifier.js.map +1 -1
  20. package/dist/core/PostUpdateMigrator.d.ts +29 -7
  21. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  22. package/dist/core/PostUpdateMigrator.js +127 -24
  23. package/dist/core/PostUpdateMigrator.js.map +1 -1
  24. package/dist/core/ReplicatedPeerStreamReader.d.ts +3 -0
  25. package/dist/core/ReplicatedPeerStreamReader.d.ts.map +1 -1
  26. package/dist/core/ReplicatedPeerStreamReader.js +35 -2
  27. package/dist/core/ReplicatedPeerStreamReader.js.map +1 -1
  28. package/dist/core/SlackReplyRelayInstaller.d.ts.map +1 -1
  29. package/dist/core/SlackReplyRelayInstaller.js +5 -0
  30. package/dist/core/SlackReplyRelayInstaller.js.map +1 -1
  31. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  32. package/dist/core/WriteDomainRegistry.js +7 -0
  33. package/dist/core/WriteDomainRegistry.js.map +1 -1
  34. package/dist/core/linuxBrowserProvision.d.ts +93 -0
  35. package/dist/core/linuxBrowserProvision.d.ts.map +1 -0
  36. package/dist/core/linuxBrowserProvision.js +254 -0
  37. package/dist/core/linuxBrowserProvision.js.map +1 -0
  38. package/dist/core/rebalanceProof.d.ts.map +1 -1
  39. package/dist/core/rebalanceProof.js +3 -1
  40. package/dist/core/rebalanceProof.js.map +1 -1
  41. package/dist/data/builtinSkillContent.d.ts +7 -2
  42. package/dist/data/builtinSkillContent.d.ts.map +1 -1
  43. package/dist/data/builtinSkillContent.js +34 -12
  44. package/dist/data/builtinSkillContent.js.map +1 -1
  45. package/dist/data/standards-guard-index.json +1 -1
  46. package/dist/data/standards-guard-index.meta.json +2 -2
  47. package/dist/data/standards-registry.meta.json +1 -1
  48. package/dist/messaging/slack/SlackAdapter.d.ts +1 -0
  49. package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
  50. package/dist/messaging/slack/SlackAdapter.js +2 -0
  51. package/dist/messaging/slack/SlackAdapter.js.map +1 -1
  52. package/dist/messaging/slack/SlackMrkdwnFormatter.d.ts +1 -1
  53. package/dist/messaging/slack/SlackMrkdwnFormatter.d.ts.map +1 -1
  54. package/dist/messaging/slack/SlackMrkdwnFormatter.js +36 -5
  55. package/dist/messaging/slack/SlackMrkdwnFormatter.js.map +1 -1
  56. package/dist/messaging/slack/threadHistoryLabel.d.ts +1 -1
  57. package/dist/messaging/slack/threadHistoryLabel.d.ts.map +1 -1
  58. package/dist/messaging/slack/threadHistoryLabel.js +3 -1
  59. package/dist/messaging/slack/threadHistoryLabel.js.map +1 -1
  60. package/dist/messaging/telegram-origin/OriginCodexModelCanary.d.ts.map +1 -1
  61. package/dist/messaging/telegram-origin/OriginCodexModelCanary.js +5 -1
  62. package/dist/messaging/telegram-origin/OriginCodexModelCanary.js.map +1 -1
  63. package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
  64. package/dist/native/instar-desktop-host +0 -0
  65. package/dist/recommended/offerPrerequisites.d.ts.map +1 -1
  66. package/dist/recommended/offerPrerequisites.js +3 -0
  67. package/dist/recommended/offerPrerequisites.js.map +1 -1
  68. package/dist/scaffold/templates.d.ts.map +1 -1
  69. package/dist/scaffold/templates.js +4 -0
  70. package/dist/scaffold/templates.js.map +1 -1
  71. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  72. package/dist/server/CapabilityIndex.js +24 -1
  73. package/dist/server/CapabilityIndex.js.map +1 -1
  74. package/dist/server/SecretDrop.d.ts +19 -0
  75. package/dist/server/SecretDrop.d.ts.map +1 -1
  76. package/dist/server/SecretDrop.js +4 -0
  77. package/dist/server/SecretDrop.js.map +1 -1
  78. package/dist/server/fileRoutes.d.ts.map +1 -1
  79. package/dist/server/fileRoutes.js +6 -0
  80. package/dist/server/fileRoutes.js.map +1 -1
  81. package/dist/server/mobileConsolePagesRoutes.d.ts +60 -0
  82. package/dist/server/mobileConsolePagesRoutes.d.ts.map +1 -0
  83. package/dist/server/mobileConsolePagesRoutes.js +473 -0
  84. package/dist/server/mobileConsolePagesRoutes.js.map +1 -0
  85. package/dist/server/mobilePairingRoutes.d.ts.map +1 -1
  86. package/dist/server/mobilePairingRoutes.js +8 -0
  87. package/dist/server/mobilePairingRoutes.js.map +1 -1
  88. package/dist/server/routes.d.ts +1 -0
  89. package/dist/server/routes.d.ts.map +1 -1
  90. package/dist/server/routes.js +90 -14
  91. package/dist/server/routes.js.map +1 -1
  92. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  93. package/dist/testing/selfActionRegistry.js +31 -0
  94. package/dist/testing/selfActionRegistry.js.map +1 -1
  95. package/package.json +1 -1
  96. package/scripts/blocking-decision-surfaces-baseline.json +29 -9
  97. package/scripts/lint-blocking-decisions-declared.mjs +1 -1
  98. package/src/data/builtin-manifest.json +66 -66
  99. package/src/data/builtinSkillContent.ts +35 -12
  100. package/src/data/standards-guard-index.json +1 -1
  101. package/src/data/standards-guard-index.meta.json +2 -2
  102. package/src/data/standards-registry.meta.json +1 -1
  103. package/src/data/state-coherence-registry.json +18 -0
  104. package/src/scaffold/templates.ts +4 -0
  105. package/src/templates/scripts/browser/chrome-close.sh +1 -1
  106. package/src/templates/scripts/browser/chrome-js.sh +1 -1
  107. package/src/templates/scripts/browser/chrome-launch.sh +1 -1
  108. package/src/templates/scripts/browser/chrome-mainworld.sh +1 -1
  109. package/src/templates/scripts/browser/chrome-renew.sh +1 -1
  110. package/src/templates/scripts/browser/chrome-unsaved.sh +1 -1
  111. package/src/templates/scripts/browser/ndrive.sh +1 -1
  112. package/src/templates/scripts/browser/x-drive.py +1594 -0
  113. package/src/templates/scripts/browser/xdrive-provision.sh +87 -0
  114. package/src/templates/scripts/browser/xdrive.sh +120 -0
  115. package/src/templates/scripts/secret-drop-retrieve.mjs +60 -7
  116. package/src/templates/scripts/slack-reply.sh +17 -1
  117. package/upgrades/1.3.1670.md +27 -0
  118. package/upgrades/1.3.1671.md +42 -0
  119. package/upgrades/proof-accepts-queued.eli16.md +20 -0
  120. package/upgrades/side-effects/codex-canary-0166.md +14 -0
  121. package/upgrades/side-effects/mobile-native-console-pages.md +24 -0
  122. package/upgrades/side-effects/normal-chrome-linux.md +84 -0
  123. package/upgrades/side-effects/peer-stream-read-cache.md +14 -0
  124. package/upgrades/side-effects/proof-accepts-queued.md +11 -0
  125. package/upgrades/side-effects/slack-native-reference-repair.md +74 -0
@@ -88,7 +88,7 @@ import { matchTestIdentity, verifyAllowTestIdentity, loadTestIdentityKey, } from
88
88
  import { readRegistryHighWater, setRegistryHighWater } from './registryHighWater.js';
89
89
  import { ITERATIVE_CONVERGING_AUDIT_SKILL_CONTENT } from '../data/builtinSkillContent.js';
90
90
  import { SUBSCRIPTION_SIGNIN_SKILL_CONTENT } from '../data/builtinSkillContent.js';
91
- import { NORMAL_CHROME_BROWSER_SKILL_CONTENT, NORMAL_CHROME_BROWSER_SKILL_WINDOWS_APPENDIX } from '../data/builtinSkillContent.js';
91
+ import { NORMAL_CHROME_BROWSER_LINUX_SECTION, NORMAL_CHROME_BROWSER_SKILL_CONTENT, NORMAL_CHROME_BROWSER_SKILL_WINDOWS_APPENDIX } from '../data/builtinSkillContent.js';
92
92
  import { ensureAgentOwnedMemoryAllHomes } from './AgentOwnedMemory.js';
93
93
  import { migrateStageBReleaseConfig, verifyBundledStageBReleaseEvidence } from './StageBActivationGate.js';
94
94
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -362,7 +362,16 @@ The Codex CLI reads ONE shared user config (\`$CODEX_HOME/config.toml\`, default
362
362
  */
363
363
  export const NORMAL_CHROME_BROWSER_SKILL_V1_SHA256 = 'b440bad6f0d65c1c248dc9f2682183486dc118c74dd188194335ab4e0d3ecbd0';
364
364
  /** Present only in the cross-platform skill text: the idempotency marker for migrateNormalChromeBrowserSkill. */
365
- export const NORMAL_CHROME_BROWSER_SKILL_MARKER = '## Drivers by operating system';
365
+ export const NORMAL_CHROME_BROWSER_SKILL_MARKER = '### Linux (xdrive.sh, the agent display)';
366
+ /** The cross-platform appendix heading. An edited copy carrying it needs only its Linux subsection changed. */
367
+ export const NORMAL_CHROME_BROWSER_SKILL_APPENDIX_HEADING = '## Drivers by operating system';
368
+ /** The Linux subsection of the macOS + Windows text, before the driver existed. */
369
+ export const NORMAL_CHROME_BROWSER_SKILL_OLD_LINUX_HEADING = '### Linux (no Windows underneath)';
370
+ /**
371
+ * SHA-256 (LF-normalized) of the macOS + Windows SKILL.md
372
+ * (tests/fixtures/skills/normal-chrome-browser-v2.md pins the bytes; the unit test re-derives this).
373
+ */
374
+ export const NORMAL_CHROME_BROWSER_SKILL_V2_SHA256 = '4577ca18462c0e7ef3f239ea8b14c55f5f3271dddd87042ecbf92ee8ad54451e';
366
375
  /** The full heading of the retired Jev review-flag-shadow card (spec retire-staged-features-2026-10-25). */
367
376
  export const JEV_REVIEW_FLAG_CARD_RETIRED_HEADING = '### Jev Review-Flag Shadow (log-only "does this reply need your review?" measurement)';
368
377
  /**
@@ -462,18 +471,18 @@ Some web tasks must happen as a specific signed-in account, and account pages (s
462
471
  }
463
472
  /** The heading PR 2283 shipped (spec normal-chrome-builtin-skill); replaced in place by the heading below. */
464
473
  export const NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V1 = '### Normal-Chrome browser (account-bound web tasks, macOS)';
465
- /** The current heading (spec normal-chrome-cross-platform); the content-sniff key for migrateClaudeMd and the shadows. */
466
- export const NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING = '### Normal-Chrome browser (account-bound web tasks, macOS + Windows)';
474
+ /** The macOS + Windows heading (spec normal-chrome-cross-platform); replaced in place by the heading below. */
475
+ export const NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V2 = '### Normal-Chrome browser (account-bound web tasks, macOS + Windows)';
476
+ /** The current heading (spec normal-chrome-linux); the content-sniff key for migrateClaudeMd and the shadows. */
477
+ export const NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING = '### Normal-Chrome browser (account-bound web tasks, macOS + Windows + Linux)';
467
478
  /**
468
- * Normal-Chrome browser awareness (specs normal-chrome-builtin-skill, normal-chrome-cross-platform).
469
- * Shared by generateClaudeMd (new agents) and migrateClaudeMd (existing agents) so the two never
470
- * drift. Content-sniff key: NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING; a section under the v1 heading
471
- * is replaced in place.
479
+ * The CLAUDE.md section exactly as the cross-platform release rendered it. Kept so the migration can
480
+ * prove the section it replaces IS the shipped text, and leave an edited one alone.
472
481
  */
473
- export function NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION() {
482
+ export function NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION_V2() {
474
483
  return `
475
484
 
476
- ${NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING}
485
+ ${NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V2}
477
486
 
478
487
  Some web tasks must happen as a specific signed-in account, and account pages (sign-in, billing, settings, usage) often refuse or behave differently for an automated browser. The \`/normal-chrome-browser\` skill is the proven method on macOS AND Windows: a NORMAL Chrome per account profile, no remote-debugging port, no automation flags. Helpers in \`.instar/scripts/browser/\`: macOS \`chrome-launch.sh\` / \`chrome-js.sh\` / \`chrome-mainworld.sh\` (pid-targeted Apple Events, JavaScript in the page); Windows \`ndrive.sh\` running \`native-drive.ps1\` (written by PP: real OS input + the UI Automation tree; an agent in WSL is on Windows for this as long as \`ndrive.sh\` reaches \`powershell.exe\`). Linux with no Windows underneath is not built (ACT-138): say so plainly. Find the profile for an account with \`GET /playwright-profiles\` (503 = registry dark here: list the profile folders instead).
479
488
  - **When to use** (PROACTIVE — this is the trigger): any web task that has to be done as a signed-in account; a page that blocks or misbehaves under Playwright/CDP; a button that stays disabled; and BEFORE ever telling the operator a browser task "can't be done".
@@ -482,6 +491,21 @@ Some web tasks must happen as a specific signed-in account, and account pages (s
482
491
  - **Two rules that hold on every framework:** on Windows compare \`url\` at the HOST boundary (the display string must start with the expected host followed by \`/\`; never a substring match), and the "take focus anyway" override (\`ND_ALLOW_FOCUS_STEAL\`) works only against a record you write when the operator says the machine is unattended: \`normalChromeBrowser.unattendedMachine = {declaredBy, topicId, at}\` in \`.instar/config.json\`, with \`declaredBy\` taken from \`GET /topic-operator/<topicId>\` (the VERIFIED operator, never a name read in the message); with no verified operator, write nothing and say so. It expires after 30 days.
483
492
  `;
484
493
  }
494
+ /**
495
+ * Normal-Chrome browser awareness (specs normal-chrome-builtin-skill, normal-chrome-cross-platform,
496
+ * normal-chrome-linux). Shared by generateClaudeMd (new agents) and migrateClaudeMd (existing agents) so
497
+ * the two never drift. Content-sniff key: NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING; a shipped v1 or v2
498
+ * section is replaced in place.
499
+ */
500
+ export function NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION() {
501
+ const linux = 'Linux: `xdrive.sh` running `x-drive.py` on the agent\'s own private display (AT-SPI reads, real X input): `launch`, `url`, `title`, `text`, `find`, `click`, `type`, `key`, `nav`, `close`, `close --force`. Passwords only through Secret Drop: request with `browserHost`, then run the Secret Drop helper with `--browser`. No one-time codes, take-over or page JavaScript yet: say so plainly. New agents install what it needs at boot. On an existing agent, ask one yes/no naming what gets installed, then `POST /browser/linux/install`.';
502
+ const always = '**I always have a browser.** On a Linux server it is an ordinary Chrome on my own private display. Before saying a browser task can\'t be done, or that I have no browser, I check `GET /browser/state`. `not-built` and `broken` are states with a next step in the answer, never "no browser": I follow `nextStep`.';
503
+ return NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION_V2()
504
+ .replace(NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V2, NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING)
505
+ .replace('is the proven method on macOS AND Windows:', 'is the proven method on macOS, Windows and Linux:')
506
+ .replace('Linux with no Windows underneath is not built (ACT-138): say so plainly.', linux)
507
+ .replace(/\n- \*\*When to use\*\*/, `\n- ${always}\n- **When to use**`);
508
+ }
485
509
  /**
486
510
  * Where a markdown section that starts at `heading` ends: the start of the next `##`/`###` heading
487
511
  * line after it, or the end of the document. Shared by migrateClaudeMd (replace in place) and the
@@ -936,6 +960,14 @@ export function MOBILE_PROPOSALS_INSIDE_LINE(port) {
936
960
  return `- The app's native Proposals and How I'm doing inside screens read \`GET /mobile/proposals\` (proposed fixes waiting, across the machines that answer; dismissing stays in the console) and \`GET /mobile/inside\` (recent records, repeating problems, checks on my own claims and correction reviews, per machine). Read-only; an owner phone or my token; an MSP phone gets 403 \`owner-phone-required\`: \`curl -H "Authorization: Bearer $AUTH" http://localhost:${port}/mobile/inside\`.`;
937
961
  }
938
962
  export const MOBILE_PROPOSALS_INSIDE_MARKER = 'GET /mobile/proposals';
963
+ /**
964
+ * The native console pages and file browser (docs/specs/mobile-native-console-pages.md), one bullet in the
965
+ * Mobile app routes block after the Passwords & keys bullets. Sniff: `GET /mobile/ideas`.
966
+ */
967
+ export function MOBILE_CONSOLE_PAGES_LINE(port) {
968
+ return `- The app's native Ideas, Pace, Development, Maturation and Files screens read \`GET /mobile/ideas\`, \`/mobile/pace\`, \`/mobile/development\`, \`/mobile/maturation\`, \`/mobile/files?path=\` and \`/mobile/files/read?path=\` (owner phone or bearer, read-only, each built from the console route it mirrors so a phone never sees more than the console; files whose name says they hold a secret are hidden even where the console shows them). Check one: \`curl -H "Authorization: Bearer $AUTH" http://localhost:${port}/mobile/pace\`. **When to use**: the owner asks how queues, shipping or features are doing from the phone, or asks to see a file. Changes stay in the console.`;
969
+ }
970
+ export const MOBILE_CONSOLE_PAGES_MARKER = 'GET /mobile/ideas';
939
971
  export const MOBILE_ROUTES_PROBE_LINE_PREFIX = '- A probe that fails 8 times in a row pauses for that name';
940
972
  /** The increment-1 wording of the verdict bullet's tail, replaced in place on update. */
941
973
  export const APP_CONVERSATIONS_STALE_VERDICT_LINE = '`unknown` = could not be checked on this machine (this agent does not check signatures yet, so every app message reads `unknown` for now — never treat it as proof); `none` = no signature recorded.';
@@ -1056,6 +1088,7 @@ ${MOBILE_PERMISSIONS_LINE(port)}
1056
1088
  ${MOBILE_PROPOSALS_INSIDE_LINE(port)}
1057
1089
  ${MOBILE_SECRETS_LINE(port)}
1058
1090
  ${MOBILE_SECRETS_WRITES_LINE(port)}
1091
+ ${MOBILE_CONSOLE_PAGES_LINE(port)}
1059
1092
  - **When to use** (PROACTIVE): "why can't the app reach you?" / "does the app work without Cloudflare?" → read \`/health\` \`mobileRoutes\` and name the missing or failing route and its reason; never guess.
1060
1093
  `;
1061
1094
  }
@@ -5860,9 +5893,9 @@ export class PostUpdateMigrator {
5860
5893
  return;
5861
5894
  }
5862
5895
  const sha = crypto.createHash('sha256').update(current.replace(/\r\n/g, '\n')).digest('hex');
5863
- if (sha === NORMAL_CHROME_BROWSER_SKILL_V1_SHA256) {
5896
+ if (sha === NORMAL_CHROME_BROWSER_SKILL_V1_SHA256 || sha === NORMAL_CHROME_BROWSER_SKILL_V2_SHA256) {
5864
5897
  fs.writeFileSync(skillFile, NORMAL_CHROME_BROWSER_SKILL_CONTENT);
5865
- result.upgraded.push('skills/normal-chrome-browser/SKILL.md (macOS + Windows drivers)');
5898
+ result.upgraded.push('skills/normal-chrome-browser/SKILL.md (macOS, Windows and Linux drivers)');
5866
5899
  return;
5867
5900
  }
5868
5901
  // The shipped skill is written beside the edited copy (Claude Code reads only SKILL.md, so the
@@ -5870,9 +5903,20 @@ export class PostUpdateMigrator {
5870
5903
  fs.writeFileSync(path.join(path.dirname(skillFile), 'SKILL.shipped.md'), NORMAL_CHROME_BROWSER_SKILL_CONTENT);
5871
5904
  // The appendix takes the file's own line endings (a CRLF copy stays CRLF throughout).
5872
5905
  const crlf = current.includes('\r\n');
5906
+ // An edited copy that already has the appendix needs only its Linux subsection changed.
5907
+ if (current.includes(NORMAL_CHROME_BROWSER_SKILL_APPENDIX_HEADING)) {
5908
+ const lf = crlf ? current.replace(/\r\n/g, '\n') : current;
5909
+ const bounds = markdownSectionBounds(lf, NORMAL_CHROME_BROWSER_SKILL_OLD_LINUX_HEADING);
5910
+ const next = bounds
5911
+ ? lf.slice(0, bounds.start) + NORMAL_CHROME_BROWSER_LINUX_SECTION + (lf.slice(bounds.end) ? '\n' + lf.slice(bounds.end) : '')
5912
+ : lf.replace(/\n*$/, '') + '\n\n' + NORMAL_CHROME_BROWSER_LINUX_SECTION;
5913
+ fs.writeFileSync(skillFile, crlf ? next.replace(/\n/g, '\r\n') : next);
5914
+ result.upgraded.push('skills/normal-chrome-browser/SKILL.md (Linux driver section added; local edits kept; shipped text in SKILL.shipped.md)');
5915
+ return;
5916
+ }
5873
5917
  const appendix = crlf ? NORMAL_CHROME_BROWSER_SKILL_WINDOWS_APPENDIX.replace(/\n/g, '\r\n') : NORMAL_CHROME_BROWSER_SKILL_WINDOWS_APPENDIX;
5874
5918
  fs.writeFileSync(skillFile, current.replace(/(\r?\n)*$/, '') + (crlf ? '\r\n\r\n' : '\n\n') + appendix);
5875
- result.upgraded.push('skills/normal-chrome-browser/SKILL.md (appended the Windows driver section; local edits kept; shipped text in SKILL.shipped.md)');
5919
+ result.upgraded.push('skills/normal-chrome-browser/SKILL.md (appended the Windows and Linux driver sections; local edits kept; shipped text in SKILL.shipped.md)');
5876
5920
  // The kept text may still teach what the shipped rules forbid (the predecessor method's synthetic
5877
5921
  // focus-event step). That is surfaced as a signal for the operator, never rewritten.
5878
5922
  if (/FocusEvent|fire events|dispatchEvent\(new (Focus|Mouse|Pointer)Event/.test(current)) {
@@ -8334,17 +8378,33 @@ setTimeout(() => process.exit(0), 2000);
8334
8378
  patched = true;
8335
8379
  result.upgraded.push('CLAUDE.md: added Memory privacy recovery');
8336
8380
  }
8381
+ // Registry First: the browser's state is a question with a place to look (normal-chrome-linux).
8382
+ if (!content.includes('| Can I use a browser?') && content.includes('| What can I do?')) {
8383
+ const row = `| Can I use a browser? / can this be done on a website? | \`curl -H "Authorization: Bearer $AUTH" http://localhost:${port}/browser/state\` |`;
8384
+ content = content.replace(/^(\| What can I do\?.*)$/gm, `$1\n${row}`);
8385
+ patched = true;
8386
+ result.upgraded.push('CLAUDE.md: added the browser state to Registry First');
8387
+ }
8337
8388
  // Replace a shipped legacy section before any missing capability bullets
8338
8389
  // are appended. An appended bullet at EOF would otherwise become part of
8339
8390
  // that section and make its exact-text migration refuse the replacement.
8340
8391
  {
8341
- const r = replaceOrAppendMarkdownSection(content, NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V1, NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION(), NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION_V1());
8342
- if (r.action !== 'present') {
8392
+ // The section actually present is the one replaced in place; a release revert can leave both,
8393
+ // and the second pass then removes whichever shipped copy is left beside the new section.
8394
+ const v2First = content.includes(NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V2);
8395
+ const passes = [
8396
+ [NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V2, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION_V2()],
8397
+ [NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V1, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION_V1()],
8398
+ ];
8399
+ for (const [oldHeading, oldSection] of v2First ? passes : passes.reverse()) {
8400
+ const r = replaceOrAppendMarkdownSection(content, oldHeading, NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION(), oldSection);
8401
+ if (r.action === 'present')
8402
+ continue;
8343
8403
  content = r.content;
8344
8404
  patched = true;
8345
- result.upgraded.push(r.action === 'replaced' ? 'CLAUDE.md: replaced Normal-Chrome browser section (macOS + Windows)'
8346
- : r.action === 'appended-old-kept' ? 'CLAUDE.md: added Normal-Chrome browser section (macOS + Windows); the edited macOS-only section was kept'
8347
- : r.action === 'removed-stale' ? 'CLAUDE.md: removed the stale macOS-only Normal-Chrome browser section'
8405
+ result.upgraded.push(r.action === 'replaced' ? 'CLAUDE.md: replaced Normal-Chrome browser section (macOS, Windows and Linux)'
8406
+ : r.action === 'appended-old-kept' ? 'CLAUDE.md: added Normal-Chrome browser section (macOS, Windows and Linux); the edited older section was kept'
8407
+ : r.action === 'removed-stale' ? 'CLAUDE.md: removed a stale older Normal-Chrome browser section'
8348
8408
  : 'CLAUDE.md: added Normal-Chrome browser section');
8349
8409
  }
8350
8410
  }
@@ -9485,6 +9545,23 @@ setTimeout(() => process.exit(0), 2000);
9485
9545
  patched = true;
9486
9546
  result.upgraded.push('CLAUDE.md: added the native Proposals and How I\'m doing inside line');
9487
9547
  }
9548
+ // Native console pages (docs/specs/mobile-native-console-pages.md): one bullet after the Passwords & keys
9549
+ // bullet of an existing Mobile app routes block (a block added below already has it).
9550
+ if (content.includes('Mobile app routes') && !content.includes(MOBILE_CONSOLE_PAGES_MARKER)) {
9551
+ const lines = content.split('\n');
9552
+ const writes = lines.findIndex((l) => l.includes(MOBILE_SECRETS_WRITES_MARKER));
9553
+ const at = writes >= 0 ? writes : lines.findIndex((l) => l.includes(MOBILE_SECRETS_MARKER));
9554
+ if (at >= 0) {
9555
+ lines.splice(at + 1, 0, MOBILE_CONSOLE_PAGES_LINE(port));
9556
+ content = lines.join('\n');
9557
+ }
9558
+ else {
9559
+ // A block without that bullet: append the line rather than skip it.
9560
+ content = `${content.replace(/\n*$/, '\n')}${MOBILE_CONSOLE_PAGES_LINE(port)}\n`;
9561
+ }
9562
+ patched = true;
9563
+ result.upgraded.push('CLAUDE.md: added the native console pages line');
9564
+ }
9488
9565
  // Mobile app routes (docs/specs/mobile-multi-route.md) — Agent Awareness + Migration Parity.
9489
9566
  if (!content.includes('Mobile app routes')) {
9490
9567
  content += MOBILE_ROUTES_CLAUDEMD_SECTION(port);
@@ -13797,6 +13874,15 @@ The user has been talking to you (possibly for days). A generic greeting like "H
13797
13874
  result.upgraded.push('CLAUDE.md: added Secret Drop --run atomic use-and-consume guidance');
13798
13875
  }
13799
13876
  }
13877
+ // A browser password has one exit and is not stored (normal-chrome-linux). Same anchor as --run.
13878
+ if (content.includes('secret-drop-retrieve.mjs') && !content.includes('Website passwords for the Linux browser')) {
13879
+ const anchor = 'The hardened script exists specifically to close that leak class (origin: 2026-05-20 incident).';
13880
+ if (content.includes(anchor)) {
13881
+ content = content.replace(anchor, anchor + "\n- **Website passwords for the Linux browser**: request with `browserHost` set to the host the operator named (the person sees it on the form), then `XD_PROFILE=<id> node .instar/scripts/secret-drop-retrieve.mjs TOKEN password --browser` with the password box focused. It types straight into that site's box and you never see it; stdout, --run and --consume are refused, and the submission is not stored, so a server restart loses it.");
13882
+ patched = true;
13883
+ result.upgraded.push('CLAUDE.md: added Secret Drop browser password guidance');
13884
+ }
13885
+ }
13800
13886
  // Secret Drop store-first durability (2026-06-04). Existing agents'
13801
13887
  // Security bullet still claims submissions are "in-memory only (never
13802
13888
  // written to disk)" — no longer true: submissions are persisted store-first
@@ -14458,7 +14544,7 @@ An agent created in a temporary folder (\`/tmp\`, \`/private/tmp\`, \`/var/folde
14458
14544
  '### Hand-started Codex: `instar codex connect <agent>`',
14459
14545
  // normal-chrome-builtin-skill: the helper scripts are framework-neutral
14460
14546
  // (.instar/scripts/browser/), so a Codex/Gemini agent on macOS uses them too.
14461
- '### Normal-Chrome browser (account-bound web tasks, macOS + Windows)', // == NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING (a literal so the parity guard can see it)
14547
+ '### Normal-Chrome browser (account-bound web tasks, macOS + Windows + Linux)', // == NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING (a literal so the parity guard can see it)
14462
14548
  // a2a-single-agent-identity: the agent identity + coherence check are server-side and
14463
14549
  // framework-agnostic (a Codex/Gemini agent runs on the same mesh and must explain the
14464
14550
  // not-provisioned posture and the adopt-from-a-sibling path honestly too).
@@ -14828,13 +14914,16 @@ An agent created in a temporary folder (\`/tmp\`, \`/private/tmp\`, \`/var/folde
14828
14914
  }
14829
14915
  let appended = refreshOriginCanaryCleanupAwareness(refreshOriginCanaryStartupAwareness(shadowContent));
14830
14916
  let mirrored = appended === shadowContent ? 0 : 1;
14831
- // A shadow that still carries the SHIPPED v1 (macOS-only) Normal-Chrome section loses it here
14832
- // (whether or not the current section is already present, so a release revert cannot leave both),
14833
- // and the marker loop below mirrors the current section once. An edited v1 section is kept.
14834
- {
14917
+ // A shadow that still carries a SHIPPED older Normal-Chrome section loses it here (whether or not
14918
+ // the current section is already present, so a release revert cannot leave both), and the marker
14919
+ // loop below mirrors the current section once. An edited older section is kept.
14920
+ for (const [oldHeading, oldSection] of [
14921
+ [NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V1, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION_V1()],
14922
+ [NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V2, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION_V2()],
14923
+ ]) {
14835
14924
  const crlf = appended.includes('\r\n');
14836
14925
  const lf = crlf ? appended.replace(/\r\n/g, '\n') : appended;
14837
- const stale = findExactMarkdownSection(lf, NORMAL_CHROME_BROWSER_CLAUDEMD_HEADING_V1, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION_V1());
14926
+ const stale = findExactMarkdownSection(lf, oldHeading, oldSection);
14838
14927
  if (stale) {
14839
14928
  const stripped = lf.slice(0, stale.start).replace(/\n*$/, '') + '\n' + lf.slice(stale.end).replace(/^\n*/, '\n');
14840
14929
  appended = crlf ? stripped.replace(/\n/g, '\r\n') : stripped;
@@ -17507,6 +17596,20 @@ if [ -n "\$NOW" ]; then
17507
17596
  echo "--- END CURRENT TIME ---"
17508
17597
  fi
17509
17598
 
17599
+ # Browser awareness on a Linux server: every session starts knowing it has one. Read from the file
17600
+ # the install writes, so it works with the server down. Never says "no browser" (normal-chrome-linux).
17601
+ if [ "\$(uname -s)" = "Linux" ] && ! command -v powershell.exe >/dev/null 2>&1; then
17602
+ BROWSER_RESULT=\$(sed -n 's/.*"status":"\\([^"]*\\)".*/\\1/p' "$INSTAR_DIR/state/xdrive/provision.json" 2>/dev/null)
17603
+ case "\$BROWSER_RESULT" in
17604
+ installing) BROWSER_STATE="installing" ;;
17605
+ failed*) BROWSER_STATE="broken" ;;
17606
+ *) if [ -x /opt/google/chrome/chrome ]; then BROWSER_STATE="ready"; else BROWSER_STATE="not-built"; fi ;;
17607
+ esac
17608
+ echo ""
17609
+ echo "BROWSER: I have a browser on this server: ordinary Chrome on my own private display (/normal-chrome-browser, xdrive.sh). State: \$BROWSER_STATE."
17610
+ echo "Before saying a browser task can't be done, check GET /browser/state. not-built and broken have a nextStep there; neither means no browser."
17611
+ fi
17612
+
17510
17613
  # TOPIC CONTEXT (loaded FIRST — highest priority context)
17511
17614
  if [ -n "\$INSTAR_TELEGRAM_TOPIC" ]; then
17512
17615
  TOPIC_ID="\$INSTAR_TELEGRAM_TOPIC"