switchroom 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +49 -57
  2. package/bin/timezone-hook.sh +9 -7
  3. package/dist/agent-scheduler/index.js +285 -45
  4. package/dist/auth-broker/index.js +13932 -0
  5. package/dist/cli/switchroom.js +15931 -12778
  6. package/dist/host-control/main.js +582 -43
  7. package/dist/vault/approvals/kernel-server.js +276 -47
  8. package/dist/vault/broker/server.js +333 -69
  9. package/examples/minimal.yaml +63 -0
  10. package/examples/personal-google-workspace-mcp/.env.example +34 -0
  11. package/examples/personal-google-workspace-mcp/README.md +194 -0
  12. package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
  13. package/examples/switchroom.yaml +220 -0
  14. package/package.json +6 -4
  15. package/profiles/_base/start.sh.hbs +3 -3
  16. package/profiles/_shared/agent-self-service.md.hbs +126 -0
  17. package/profiles/default/CLAUDE.md +10 -0
  18. package/profiles/default/CLAUDE.md.hbs +16 -0
  19. package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
  20. package/skills/buildkite-agent-runtime/SKILL.md +44 -11
  21. package/skills/buildkite-api/SKILL.md +31 -8
  22. package/skills/buildkite-cli/SKILL.md +27 -9
  23. package/skills/buildkite-migration/SKILL.md +22 -9
  24. package/skills/buildkite-pipelines/SKILL.md +26 -9
  25. package/skills/buildkite-secure-delivery/SKILL.md +23 -9
  26. package/skills/buildkite-test-engine/SKILL.md +25 -8
  27. package/skills/docx/SKILL.md +1 -1
  28. package/skills/file-bug/SKILL.md +34 -6
  29. package/skills/humanizer/SKILL.md +15 -0
  30. package/skills/humanizer-calibrate/SKILL.md +7 -1
  31. package/skills/mcp-builder/SKILL.md +1 -1
  32. package/skills/pdf/SKILL.md +1 -1
  33. package/skills/pptx/SKILL.md +1 -1
  34. package/skills/skill-creator/SKILL.md +21 -1
  35. package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  36. package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
  37. package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
  38. package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
  39. package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
  40. package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  41. package/skills/switchroom-cli/SKILL.md +63 -64
  42. package/skills/switchroom-health/SKILL.md +23 -10
  43. package/skills/switchroom-install/SKILL.md +3 -3
  44. package/skills/switchroom-manage/SKILL.md +26 -19
  45. package/skills/switchroom-runtime/SKILL.md +67 -15
  46. package/skills/switchroom-status/SKILL.md +26 -1
  47. package/skills/telegram-test-harness/SKILL.md +3 -0
  48. package/skills/webapp-testing/SKILL.md +31 -1
  49. package/skills/xlsx/SKILL.md +1 -1
  50. package/telegram-plugin/admin-commands/index.ts +7 -5
  51. package/telegram-plugin/dist/gateway/gateway.js +13042 -12844
  52. package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
  53. package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
  54. package/telegram-plugin/gateway/auth-command.ts +794 -0
  55. package/telegram-plugin/gateway/auth-line.ts +123 -0
  56. package/telegram-plugin/gateway/boot-card.ts +22 -36
  57. package/telegram-plugin/gateway/boot-probes.ts +3 -3
  58. package/telegram-plugin/gateway/gateway.ts +313 -798
  59. package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
  60. package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
  61. package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
  62. package/telegram-plugin/permission-title.ts +56 -0
  63. package/telegram-plugin/quota-check.ts +19 -41
  64. package/telegram-plugin/scripts/build.mjs +0 -1
  65. package/telegram-plugin/shared/bot-runtime.ts +5 -4
  66. package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
  67. package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
  68. package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
  69. package/telegram-plugin/tests/boot-probes.test.ts +11 -4
  70. package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
  71. package/telegram-plugin/tests/permission-title.test.ts +31 -0
  72. package/telegram-plugin/tests/quota-check.test.ts +5 -35
  73. package/telegram-plugin/uat/SETUP.md +31 -1
  74. package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
  75. package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
  76. package/telegram-plugin/uat/runners/report.ts +150 -0
  77. package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
  78. package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
  79. package/telegram-plugin/uat/runners/scorer.ts +106 -0
  80. package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
  81. package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
  82. package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +7 -1
  83. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +7 -1
  84. package/telegram-plugin/auth-dashboard.ts +0 -1104
  85. package/telegram-plugin/auth-slot-parser.ts +0 -497
  86. package/telegram-plugin/dist/foreman/foreman.js +0 -31358
  87. package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
  88. package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
  89. package/telegram-plugin/foreman/foreman.ts +0 -1165
  90. package/telegram-plugin/foreman/setup-flow.ts +0 -345
  91. package/telegram-plugin/foreman/setup-state.ts +0 -239
  92. package/telegram-plugin/foreman/state.ts +0 -203
  93. package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
  94. package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
  95. package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
  96. package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
  97. package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
  98. package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
  99. package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
  100. package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
  101. package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
  102. package/telegram-plugin/tests/foreman-state.test.ts +0 -164
  103. package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
  104. package/telegram-plugin/tests/setup-flow.test.ts +0 -510
  105. package/telegram-plugin/tests/setup-state.test.ts +0 -146
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Boot-card auth row formatter (RFC H §7.3).
3
+ *
4
+ * The old auth-dashboard exported `formatAccountQuotaLine` + an
5
+ * `AccountSummary` shape that the boot card consumed for its
6
+ * "Accounts (N)" section. Both source-of-truth and shape moved to
7
+ * the auth-broker's `list-state` response. This module reformats that
8
+ * response into the same one-line-per-account block the boot card
9
+ * used to render — visual output unchanged, data source is now the
10
+ * broker.
11
+ *
12
+ * Inputs: a `list-state` data shape (see
13
+ * `src/auth/broker/protocol.ts` → `ListStateDataSchema`) plus the
14
+ * caller agent's name.
15
+ *
16
+ * Output: an array of HTML-safe lines. Empty array when there's
17
+ * nothing to show — preserves the boot-card's silent-when-healthy
18
+ * default.
19
+ */
20
+
21
+ import type { ListStateData, AccountState } from '../../src/auth/broker/client.js'
22
+
23
+ export type { ListStateData, AccountState }
24
+
25
+ // Local HTML-escape (mirrors the helper formerly co-located in
26
+ // auth-dashboard.ts so we keep the same escaping discipline without
27
+ // pulling in a heavier util).
28
+ function escapeHtml(s: string): string {
29
+ return s
30
+ .replace(/&/g, '&')
31
+ .replace(/</g, '&lt;')
32
+ .replace(/>/g, '&gt;')
33
+ }
34
+
35
+ /** Format a duration in ms as a short relative string ("1h 22m", "12s"). */
36
+ function formatRelativeMs(ms: number): string {
37
+ if (ms <= 0) return '0s'
38
+ const totalSec = Math.floor(ms / 1000)
39
+ const days = Math.floor(totalSec / 86400)
40
+ const hours = Math.floor((totalSec % 86400) / 3600)
41
+ const mins = Math.floor((totalSec % 3600) / 60)
42
+ const secs = totalSec % 60
43
+ if (days > 0) return `${days}d ${hours}h`
44
+ if (hours > 0) return `${hours}h ${mins}m`
45
+ if (mins > 0) return `${mins}m ${secs}s`
46
+ return `${secs}s`
47
+ }
48
+
49
+ /**
50
+ * Render the per-account quota inline for one account row. Returns
51
+ * null when there's nothing quota-shaped to say (account is healthy
52
+ * and we have no reset countdown to surface).
53
+ */
54
+ export function formatAuthQuotaLine(acc: AccountState, now: number = Date.now()): string | null {
55
+ if (acc.exhausted) {
56
+ const until = acc.exhausted_until
57
+ if (until != null && until > now) {
58
+ return `<i>exhausted · resets in ${formatRelativeMs(until - now)}</i>`
59
+ }
60
+ return `<i>exhausted</i>`
61
+ }
62
+ return null
63
+ }
64
+
65
+ /**
66
+ * Boot-card auth-row block.
67
+ *
68
+ * Strategy:
69
+ * 1. Determine *which* account is active for `agentName` (per-agent
70
+ * override wins over fleet-active).
71
+ * 2. Emit one row for that account marked with `▶` plus a
72
+ * best-effort quota suffix.
73
+ * 3. Emit one row per other account in `fallback_order` marked with
74
+ * `↳` so the operator sees the rollover plan at a glance.
75
+ *
76
+ * Returns an empty array when `state` is empty (no accounts) — the
77
+ * boot card's silent-when-healthy contract.
78
+ */
79
+ export function renderAuthLine(
80
+ state: ListStateData,
81
+ agentName: string,
82
+ now: number = Date.now(),
83
+ ): string[] {
84
+ if (!state || state.accounts.length === 0) return []
85
+
86
+ const agentEntry = state.agents.find((a) => a.name === agentName)
87
+ const activeLabel = agentEntry?.override ?? agentEntry?.account ?? state.active
88
+
89
+ // Stable display order: active first, then `fallback_order` minus
90
+ // the active label, then any remaining accounts (defensive — should
91
+ // be empty in steady state) in account-list order.
92
+ const seen = new Set<string>()
93
+ const order: string[] = []
94
+ if (activeLabel) {
95
+ order.push(activeLabel)
96
+ seen.add(activeLabel)
97
+ }
98
+ for (const label of state.fallback_order) {
99
+ if (!seen.has(label)) {
100
+ order.push(label)
101
+ seen.add(label)
102
+ }
103
+ }
104
+ for (const acc of state.accounts) {
105
+ if (!seen.has(acc.label)) {
106
+ order.push(acc.label)
107
+ seen.add(acc.label)
108
+ }
109
+ }
110
+
111
+ const byLabel = new Map(state.accounts.map((a) => [a.label, a]))
112
+ const rows: string[] = []
113
+ rows.push(`<b>Accounts (${state.accounts.length})</b>`)
114
+ for (const label of order) {
115
+ const acc = byLabel.get(label)
116
+ if (!acc) continue
117
+ const marker = label === activeLabel ? '▶' : '↳'
118
+ const labelHtml = `<code>${escapeHtml(acc.label)}</code>`
119
+ const quotaLine = formatAuthQuotaLine(acc, now)
120
+ rows.push(quotaLine ? `${marker} ${labelHtml} ${quotaLine}` : `${marker} ${labelHtml}`)
121
+ }
122
+ return rows
123
+ }
@@ -33,8 +33,8 @@
33
33
  */
34
34
 
35
35
  import type { ProbeResult, GatewayRuntimeInfo } from './boot-probes.js'
36
- import type { AccountSummary } from '../auth-dashboard.js'
37
- import { formatAccountQuotaLine } from '../auth-dashboard.js'
36
+ import type { ListStateData } from './auth-line.js'
37
+ import { renderAuthLine } from './auth-line.js'
38
38
  import {
39
39
  probeAccount,
40
40
  probeAgentProcess,
@@ -274,9 +274,12 @@ export interface RenderBootCardOpts {
274
274
  * silent-when-healthy contract for callers that don't pass account
275
275
  * data (tests, harnesses, gateways without the auth model).
276
276
  *
277
- * Closes #708.
277
+ * Post-RFC H (auth-broker rewire): this carries the broker's
278
+ * `list-state` shape — `renderAuthLine` consumes it to emit the
279
+ * same one-line-per-account rows as before. Callers that pass
280
+ * `null`/`undefined` get no section. Closes #708.
278
281
  */
279
- accounts?: ReadonlyArray<AccountSummary>
282
+ accounts?: ListStateData | null
280
283
  /** Probe keys for which the prior boot saw degraded/fail and this boot
281
284
  * sees ok. Rendered as a small ✅ line above the degraded section so
282
285
  * the user gets positive-feedback that a known issue is gone. */
@@ -380,11 +383,13 @@ export function renderBootCard(opts: RenderBootCardOpts): string {
380
383
  }
381
384
  }
382
385
 
383
- // Per-account quota section (issue #708) — one line per enabled
384
- // account showing 5h % / 7d % / nearest reset, with the active
385
- // account marked. Renders alongside the ack line so users see
386
- // headroom without running /auth or /usage.
387
- const accountRows = renderAccountRows(opts.accounts, opts.now ?? new Date())
386
+ // Per-account auth section (issue #708, RFC H rewire) — one line
387
+ // per known account with the active account marked. Renders
388
+ // alongside the ack line so users see headroom without running
389
+ // /auth or /usage. Source of truth: auth-broker list-state.
390
+ const accountRows = opts.accounts
391
+ ? renderAuthLine(opts.accounts, agentName, (opts.now ?? new Date()).getTime())
392
+ : []
388
393
 
389
394
  const sections: string[] = [ack]
390
395
  if (degradedRows.length > 0) sections.push('', ...degradedRows)
@@ -394,31 +399,12 @@ export function renderBootCard(opts: RenderBootCardOpts): string {
394
399
  }
395
400
 
396
401
  /**
397
- * Render the per-account quota rows. Returns an empty array when no
398
- * accounts are passed keeping the boot card's silent-when-healthy
399
- * default for callers that don't supply account data.
400
- *
401
- * Reuses the dashboard's `formatAccountQuotaLine` so the two surfaces
402
- * speak with one voice.
402
+ * Re-export the broker-fed auth-row renderer under its historical
403
+ * name so direct callers (tests, harnesses) keep working without
404
+ * importing two modules. New code should import `renderAuthLine`
405
+ * from `./auth-line.js` directly.
403
406
  */
404
- export function renderAccountRows(
405
- accounts: ReadonlyArray<AccountSummary> | undefined,
406
- now: Date,
407
- ): string[] {
408
- if (!accounts || accounts.length === 0) return []
409
- const rows: string[] = []
410
- rows.push(`<b>Accounts (${accounts.length})</b>`)
411
- const nowMs = now.getTime()
412
- for (const a of accounts) {
413
- const marker = a.activeForThisAgent ? '▶' : '↳'
414
- const labelHtml = `<code>${escapeHtml(a.label)}</code>`
415
- // formatAccountQuotaLine returns HTML (with <i> tags) so we don't
416
- // re-escape — pass it through verbatim.
417
- const quotaLine = formatAccountQuotaLine(a, nowMs)
418
- rows.push(quotaLine ? `${marker} ${labelHtml} ${quotaLine}` : `${marker} ${labelHtml}`)
419
- }
420
- return rows
421
- }
407
+ export { renderAuthLine as renderAccountRows } from './auth-line.js'
422
408
 
423
409
  // ─── Probe orchestration ─────────────────────────────────────────────────────
424
410
 
@@ -480,9 +466,9 @@ export interface RunProbesOpts {
480
466
  * during the post-settle re-render so the first paint stays fast.
481
467
  */
482
468
  loadAccounts?: () =>
483
- | ReadonlyArray<AccountSummary>
469
+ | ListStateData
484
470
  | null
485
- | Promise<ReadonlyArray<AccountSummary> | null>
471
+ | Promise<ListStateData | null>
486
472
  /** When true, resolve the agent PID via cgroup walk instead of MainPID
487
473
  * (which is the tmux server pid under tmux supervisor). */
488
474
  tmuxSupervisor?: boolean
@@ -604,7 +590,7 @@ export async function startBootCard(
604
590
  // Per-account rows (issue #708). Loaded best-effort
605
591
  // alongside probes; failures are swallowed so the card still
606
592
  // renders correctly with no accounts section.
607
- let accountRows: ReadonlyArray<AccountSummary> | null = null
593
+ let accountRows: ListStateData | null = null
608
594
  if (opts.loadAccounts) {
609
595
  try {
610
596
  accountRows = await opts.loadAccounts()
@@ -861,7 +861,7 @@ export async function probeQuota(
861
861
  status: 'degraded',
862
862
  label: 'Quota',
863
863
  detail: 'no OAuth token',
864
- nextStep: 'No OAuth token on disk — run `switchroom auth login <agent>` to authenticate',
864
+ nextStep: 'No OAuth token on disk — register a fleet account: `switchroom auth add <label> --from-oauth` then `switchroom auth use <label>` (RFC H)',
865
865
  }
866
866
  }
867
867
 
@@ -880,8 +880,8 @@ export async function probeQuota(
880
880
  label: 'Quota',
881
881
  detail: probe.reason,
882
882
  nextStep: isAuth
883
- ? 'Auth rejected by Anthropic — re-authenticate with `switchroom auth login <agent>`'
884
- : 'Anthropic quota probe failed — re-check after a minute; if persistent, `switchroom auth login <agent>`',
883
+ ? 'Auth rejected by Anthropic — broker auto-refreshes; if persistent, replace the account: `switchroom auth add <label> --from-oauth --replace`'
884
+ : 'Anthropic quota probe failed — re-check after a minute; broker auto-rotates per `auth.fallback_order`',
885
885
  }
886
886
  }
887
887