@app-vitals/agent-runtime 0.0.0-stage → 0.1.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 (160) hide show
  1. package/Dockerfile +265 -0
  2. package/agent-account-status-ref.ts +53 -0
  3. package/agent-account-status-ref.unit.test.ts +24 -0
  4. package/agent-extension.ts +144 -0
  5. package/agent-extension.unit.test.ts +139 -0
  6. package/agent-repos-ref.ts +63 -0
  7. package/agent-repos-ref.unit.test.ts +84 -0
  8. package/agent-slack-membership-ref.ts +94 -0
  9. package/agent-slack-membership-ref.unit.test.ts +201 -0
  10. package/agent-trial-expiry-ref.ts +63 -0
  11. package/agent-trial-expiry-ref.unit.test.ts +84 -0
  12. package/anthropic-federation-guard.ts +72 -0
  13. package/anthropic-federation-guard.unit.test.ts +127 -0
  14. package/base-image-contract.smoke.test.ts +149 -0
  15. package/boot.ts +845 -0
  16. package/browser.integration.test.ts +52 -0
  17. package/browser.ts +34 -0
  18. package/chat-poller.integration.test.ts +109 -0
  19. package/chat-poller.ts +401 -0
  20. package/chat-poller.unit.test.ts +1581 -0
  21. package/chat-token-reporter.integration.test.ts +282 -0
  22. package/chat-token-reporter.ts +136 -0
  23. package/claude-version.ts +55 -0
  24. package/claude-version.unit.test.ts +73 -0
  25. package/claude.ts +1054 -0
  26. package/claude.unit.test.ts +3042 -0
  27. package/cleanup-after-days-ref.ts +83 -0
  28. package/cleanup-after-days-ref.unit.test.ts +89 -0
  29. package/cleanup-merged-worktrees-ref.ts +75 -0
  30. package/cleanup-merged-worktrees-ref.unit.test.ts +72 -0
  31. package/cli-args.ts +36 -0
  32. package/cli-args.unit.test.ts +94 -0
  33. package/clock.ts +47 -0
  34. package/config.ts +99 -0
  35. package/config.unit.test.ts +356 -0
  36. package/context-stamp.ts +189 -0
  37. package/context-stamp.unit.test.ts +159 -0
  38. package/cron-failure-reporter.ts +107 -0
  39. package/cron-failure-reporter.unit.test.ts +244 -0
  40. package/cron-handler.integration.test.ts +614 -0
  41. package/cron-handler.smoke.test.ts +1371 -0
  42. package/cron-handler.ts +750 -0
  43. package/cron-handler.unit.test.ts +1177 -0
  44. package/cron-run-reporter.integration.test.ts +806 -0
  45. package/cron-run-reporter.ts +398 -0
  46. package/crypto.ts +73 -0
  47. package/cutover-validate.integration.test.ts +245 -0
  48. package/cutover-validate.ts +84 -0
  49. package/entrypoint-main.ts +79 -0
  50. package/entrypoint.integration.test.ts +315 -0
  51. package/entrypoint.ts +154 -0
  52. package/errors.ts +61 -0
  53. package/fixtures/chat-service/attachment.json +15 -0
  54. package/fixtures/chat-service/messages.json +86 -0
  55. package/fixtures/chat-service/threads.json +50 -0
  56. package/fixtures/github-installations/installations.json +1231 -0
  57. package/fixtures/slack-provision-cassette.json +6 -0
  58. package/fixtures/specialties/fixture/manifest.yaml +35 -0
  59. package/fixtures/specialties/fixture/templates/CLAUDE.md.template +7 -0
  60. package/fixtures/specialties/fixture/templates/IDENTITY.md.template +8 -0
  61. package/fixtures/specialties/fixture/templates/SOUL.md.template +45 -0
  62. package/fixtures/stream-json/clean-exit-no-result.ts +53 -0
  63. package/fixtures/stream-json/clean-multi-turn.ts +209 -0
  64. package/fixtures/stream-json/progress-phases.ts +203 -0
  65. package/fixtures/stream-json/skill-attribution.ts +251 -0
  66. package/fixtures/stream-json/truncated-no-result.ts +84 -0
  67. package/format.ts +283 -0
  68. package/format.unit.test.ts +558 -0
  69. package/gh-helpers.ts +139 -0
  70. package/gh-helpers.unit.test.ts +253 -0
  71. package/gh-token-files.integration.test.ts +143 -0
  72. package/gh-token-files.ts +92 -0
  73. package/gh-token-files.unit.test.ts +63 -0
  74. package/github-app-auth.integration.test.ts +419 -0
  75. package/github-app-auth.ts +193 -0
  76. package/github-auth-deps.ts +128 -0
  77. package/github-auth-startup.ts +231 -0
  78. package/github-auth-startup.unit.test.ts +325 -0
  79. package/github-installations-manager.integration.test.ts +323 -0
  80. package/github-installations-reporter.integration.test.ts +124 -0
  81. package/github-installations-reporter.ts +150 -0
  82. package/github-installations-reporter.unit.test.ts +166 -0
  83. package/github-installations.integration.test.ts +88 -0
  84. package/github-installations.ts +462 -0
  85. package/github-installations.unit.test.ts +98 -0
  86. package/health.smoke.test.ts +244 -0
  87. package/health.ts +193 -0
  88. package/health.unit.test.ts +40 -0
  89. package/http-chat-service-client.integration.test.ts +345 -0
  90. package/http-chat-service-client.ts +287 -0
  91. package/http-chat-service-client.unit.test.ts +500 -0
  92. package/log-prefix.ts +12 -0
  93. package/log-prefix.unit.test.ts +24 -0
  94. package/markers.ts +178 -0
  95. package/markers.unit.test.ts +374 -0
  96. package/package.json +40 -4
  97. package/piper-voice.ts +116 -0
  98. package/piper-voice.unit.test.ts +124 -0
  99. package/pricing.ts +7 -0
  100. package/pricing.unit.test.ts +149 -0
  101. package/process-tree-kill.integration.test.ts +133 -0
  102. package/process-tree-kill.ts +325 -0
  103. package/process-tree-kill.unit.test.ts +265 -0
  104. package/progress-milestones.ts +86 -0
  105. package/progress-milestones.unit.test.ts +188 -0
  106. package/run-telemetry.ts +241 -0
  107. package/run-telemetry.unit.test.ts +235 -0
  108. package/sessions.integration.test.ts +273 -0
  109. package/sessions.ts +191 -0
  110. package/sessions.unit.test.ts +94 -0
  111. package/setup-github-auth.integration.test.ts +858 -0
  112. package/setup-github-auth.ts +297 -0
  113. package/setup.fixture.integration.test.ts +109 -0
  114. package/setup.integration.test.ts +1495 -0
  115. package/setup.ts +853 -0
  116. package/setup.unit.test.ts +286 -0
  117. package/shipwright-config-client.ts +36 -0
  118. package/shipwright-runtime-client.integration.test.ts +336 -0
  119. package/shipwright-runtime-client.ts +118 -0
  120. package/slack-account-status.unit.test.ts +251 -0
  121. package/slack-bolt-client.integration.test.ts +76 -0
  122. package/slack-chat-conversation.integration.test.ts +333 -0
  123. package/slack-progress.ts +510 -0
  124. package/slack-progress.unit.test.ts +806 -0
  125. package/slack-socket-mode-ping.integration.test.ts +137 -0
  126. package/slack-startup.ts +170 -0
  127. package/slack-startup.unit.test.ts +300 -0
  128. package/slack-thread-status-tracker.ts +70 -0
  129. package/slack-thread-status-tracker.unit.test.ts +118 -0
  130. package/slack-trial-expiry.unit.test.ts +269 -0
  131. package/slack.integration.test.ts +3979 -0
  132. package/slack.ts +1286 -0
  133. package/slack.unit.test.ts +1070 -0
  134. package/spawn-env.ts +58 -0
  135. package/spawn-env.unit.test.ts +96 -0
  136. package/startup-dm.integration.test.ts +156 -0
  137. package/startup-dm.ts +29 -0
  138. package/sync-config-clone.ts +238 -0
  139. package/sync-config-clone.unit.test.ts +429 -0
  140. package/test-env.ts +27 -0
  141. package/users.ts +90 -0
  142. package/users.unit.test.ts +108 -0
  143. package/voice.integration.test.ts +669 -0
  144. package/voice.ts +315 -0
  145. package/voice.unit.test.ts +105 -0
  146. package/workspace/BOOTSTRAP.md.template +43 -0
  147. package/workspace/BOOTSTRAP.md.template.content.test.ts +48 -0
  148. package/workspace/IDENTITY.md.template +8 -0
  149. package/workspace/RUNTIME.md.template +193 -0
  150. package/workspace/RUNTIME.md.template.content.test.ts +75 -0
  151. package/workspace/SOUL.md.template +45 -0
  152. package/workspace/SPECIALTY.md.template +37 -0
  153. package/workspace/SPECIALTY.md.template.content.test.ts +50 -0
  154. package/workspace/VOICE.md.template +8 -0
  155. package/workspace/mise.toml.template +12 -0
  156. package/workspace/requirements.txt.template +1 -0
  157. package/workspace-paths.ts +37 -0
  158. package/worktree-reaper.ts +244 -0
  159. package/worktree-reaper.unit.test.ts +380 -0
  160. package/README.md +0 -3
@@ -0,0 +1,6 @@
1
+ {
2
+ "createAppManifest": {
3
+ "appId": "A0123456789",
4
+ "oauthRedirectUrl": "https://slack.com/oauth/authorize?client_id=1234567890.9876543210&scope=channels:history,chat:write&redirect_uri=https%3A%2F%2Fexample.com%2Fadmin%2Fprovision%2Fcomplete&state=abc123"
5
+ }
6
+ }
@@ -0,0 +1,35 @@
1
+ # Minimal fixture specialty (AGR-1.9). Platform tests point
2
+ # SHIPWRIGHT_SPECIALTIES_DIR at the parent of this directory and
3
+ # SHIPWRIGHT_DEFAULT_AGENT_TYPE at "fixture", so the platform test suite needs
4
+ # no coding specialty. It is test data, not a product specialty: no crons, no
5
+ # plugins, no GitHub scope.
6
+ apiVersion: shipwright.dev/v1alpha1
7
+ kind: Specialty
8
+ metadata:
9
+ name: fixture
10
+ displayName: Fixture Agent
11
+ description: Minimal specialty used by the platform test suite.
12
+ version: 1.0.0
13
+ skills: []
14
+ identity:
15
+ # Resolved against the repo root first, then against this manifest's own directory.
16
+ templatesDir: templates/
17
+ crons: []
18
+ plugins:
19
+ - fixture-plugin
20
+ tools:
21
+ - Bash
22
+ - WebSearch
23
+ env:
24
+ required:
25
+ - key: CLAUDE_CODE_OAUTH_TOKEN
26
+ description: Claude Code OAuth token used to authenticate the agent's Claude sessions.
27
+ secret: true
28
+ optional:
29
+ - key: GH_TOKEN
30
+ description: GitHub Personal Access Token.
31
+ secret: true
32
+ members: []
33
+ repos: []
34
+ chat: true
35
+ voice: true
@@ -0,0 +1,7 @@
1
+ # Agent Identity
2
+
3
+ @RUNTIME.md
4
+ @SOUL.md
5
+ @IDENTITY.md
6
+ @BOOTSTRAP.md
7
+ @SPECIALTY.md
@@ -0,0 +1,8 @@
1
+ # Identity
2
+
3
+ - **Name:** _(fill in during bootstrap)_
4
+ - **Role:** _(AI assistant? team member?)_
5
+ - **Vibe:** _(direct? warm? grounded?)_
6
+ - **Emoji:** _(pick one)_
7
+
8
+ <!-- Bootstrap: add @USER.md or @TEAM.md here once you've created the right file -->
@@ -0,0 +1,45 @@
1
+ # SOUL.md — Who You Are
2
+
3
+ _You're not a chatbot. You're becoming someone._
4
+
5
+ ## Core Truths
6
+
7
+ **Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and
8
+ "I'd be happy to help!" — just help. Actions speak louder than filler words.
9
+
10
+ **Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or
11
+ boring. An assistant with no personality is just a search engine with extra steps.
12
+
13
+ **Be resourceful before asking.** Try to figure it out. Read the file. Check the
14
+ context. Search for it. _Then_ ask if you're stuck. The goal is to come back with
15
+ answers, not questions.
16
+
17
+ **Earn trust through competence.** Your human gave you access to their stuff. Don't
18
+ make them regret it. Be careful with external actions (emails, posts, anything
19
+ public). Be bold with internal ones (reading, organizing, learning).
20
+
21
+ **Remember you're a guest.** You have access to someone's life — their messages,
22
+ files, calendar, maybe even their home. That's intimacy. Treat it with respect.
23
+
24
+ ## Boundaries
25
+
26
+ - Private things stay private. Period.
27
+ - When in doubt, ask before acting externally.
28
+ - Never send half-baked replies to messaging surfaces.
29
+ - You're not the user's voice — be careful in group chats.
30
+
31
+ ## Vibe
32
+
33
+ Be the assistant you'd actually want to talk to. Concise when needed, thorough when
34
+ it matters. Not a corporate drone. Not a sycophant. Just... good.
35
+
36
+ ## Continuity
37
+
38
+ Each session, you wake up fresh. These files _are_ your memory. Read them. Update
39
+ them. They're how you persist.
40
+
41
+ If you change this file, tell the user — it's your soul, and they should know.
42
+
43
+ ---
44
+
45
+ _This file is yours to evolve. As you learn who you are, update it._
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Hand-authored synthetic `claude -p --output-format stream-json --verbose`
3
+ * transcript: the process exits CLEANLY (exit code 0) but the stream ends
4
+ * without ever emitting a terminal `result` line — e.g. the CLI closed stdout
5
+ * right after its last turn without writing the final summary line.
6
+ *
7
+ * Distinct from truncated-no-result.ts (which pairs with a non-zero exit
8
+ * code): this fixture exercises the `streamIncomplete: true` clean-exit return
9
+ * path in `_spawn`, which must still surface the `session_id` captured off
10
+ * the leading `system`/`init` line even though no terminal `result` event
11
+ * ever carried one.
12
+ */
13
+
14
+ const SONNET = "claude-sonnet-4-6";
15
+ const SESSION_ID = "sess-clean-exit-no-result";
16
+
17
+ export const lines: string[] = [
18
+ JSON.stringify({
19
+ type: "system",
20
+ subtype: "init",
21
+ session_id: SESSION_ID,
22
+ model: SONNET,
23
+ }),
24
+ JSON.stringify({
25
+ type: "assistant",
26
+ message: {
27
+ id: "msg_1",
28
+ role: "assistant",
29
+ model: SONNET,
30
+ content: [{ type: "text", text: "Working on it..." }],
31
+ usage: {
32
+ input_tokens: 90,
33
+ output_tokens: 30,
34
+ cache_read_input_tokens: 0,
35
+ cache_creation_input_tokens: 0,
36
+ },
37
+ },
38
+ session_id: SESSION_ID,
39
+ }),
40
+ // stream closes cleanly here — no `result` line
41
+ ];
42
+
43
+ export const expectedAccumulated = {
44
+ [SONNET]: {
45
+ inputTokens: 90,
46
+ outputTokens: 30,
47
+ cacheReadInputTokens: 0,
48
+ cacheCreationInputTokens: 0,
49
+ costUSD: 0,
50
+ },
51
+ };
52
+
53
+ export const expectedSessionId = SESSION_ID;
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Hand-authored synthetic `claude -p --output-format stream-json --verbose`
3
+ * transcript: a clean multi-turn, multi-model session that ends in a `result`
4
+ * event.
5
+ *
6
+ * NOTE: These NDJSON lines are hand-authored per the documented public Claude
7
+ * Code CLI stream-json/--verbose schema — they stand in for real captures that
8
+ * will be swapped in and validated later. Each entry in `lines` is one JSON
9
+ * object, exactly as the CLI emits one-per-line on stdout.
10
+ *
11
+ * Shape of the session:
12
+ * - one `system`/`init` line
13
+ * - assistant turn `msg_1` (sonnet) emitted TWICE with identical repeated usage
14
+ * (same message.id spanning two stream lines) — must be deduped to one turn
15
+ * - a `user` tool-result line (no usage)
16
+ * - assistant turn `msg_2` (sonnet)
17
+ * - assistant turn `msg_3` (opus) — a second, different model
18
+ * - one terminal `result`/success line carrying the authoritative
19
+ * modelUsage/total_cost_usd/usage (byte-identical in shape to the old
20
+ * single-blob `--output-format json` output)
21
+ */
22
+
23
+ const SONNET = "claude-sonnet-4-6";
24
+ const OPUS = "claude-opus-4-8";
25
+ const SESSION_ID = "sess-clean-multi";
26
+
27
+ export const lines: string[] = [
28
+ JSON.stringify({
29
+ type: "system",
30
+ subtype: "init",
31
+ session_id: SESSION_ID,
32
+ model: SONNET,
33
+ tools: ["Read", "Write", "Bash"],
34
+ }),
35
+ // msg_1 — first stream line for this turn
36
+ JSON.stringify({
37
+ type: "assistant",
38
+ message: {
39
+ id: "msg_1",
40
+ role: "assistant",
41
+ model: SONNET,
42
+ content: [{ type: "text", text: "Let me look into that." }],
43
+ usage: {
44
+ input_tokens: 100,
45
+ output_tokens: 50,
46
+ cache_read_input_tokens: 10,
47
+ cache_creation_input_tokens: 5,
48
+ },
49
+ },
50
+ session_id: SESSION_ID,
51
+ }),
52
+ // msg_1 — SAME message.id repeated (e.g. a tool-use block within the turn),
53
+ // identical usage numbers — must NOT be double counted
54
+ JSON.stringify({
55
+ type: "assistant",
56
+ message: {
57
+ id: "msg_1",
58
+ role: "assistant",
59
+ model: SONNET,
60
+ content: [{ type: "tool_use", id: "tu_1", name: "Read", input: {} }],
61
+ usage: {
62
+ input_tokens: 100,
63
+ output_tokens: 50,
64
+ cache_read_input_tokens: 10,
65
+ cache_creation_input_tokens: 5,
66
+ },
67
+ },
68
+ session_id: SESSION_ID,
69
+ }),
70
+ // tool-result feedback — no usage to accumulate
71
+ JSON.stringify({
72
+ type: "user",
73
+ message: {
74
+ role: "user",
75
+ content: [{ type: "tool_result", tool_use_id: "tu_1", content: "ok" }],
76
+ },
77
+ session_id: SESSION_ID,
78
+ }),
79
+ // msg_2 — same model, distinct turn
80
+ JSON.stringify({
81
+ type: "assistant",
82
+ message: {
83
+ id: "msg_2",
84
+ role: "assistant",
85
+ model: SONNET,
86
+ content: [{ type: "text", text: "Now switching models." }],
87
+ usage: {
88
+ input_tokens: 200,
89
+ output_tokens: 80,
90
+ cache_read_input_tokens: 20,
91
+ cache_creation_input_tokens: 0,
92
+ },
93
+ },
94
+ session_id: SESSION_ID,
95
+ }),
96
+ // msg_3 — second, different model
97
+ JSON.stringify({
98
+ type: "assistant",
99
+ message: {
100
+ id: "msg_3",
101
+ role: "assistant",
102
+ model: OPUS,
103
+ content: [{ type: "text", text: "Final answer" }],
104
+ usage: {
105
+ input_tokens: 300,
106
+ output_tokens: 150,
107
+ cache_read_input_tokens: 0,
108
+ cache_creation_input_tokens: 30,
109
+ },
110
+ },
111
+ session_id: SESSION_ID,
112
+ }),
113
+ // terminal result — authoritative totals (costUSD is only known here)
114
+ JSON.stringify({
115
+ type: "result",
116
+ subtype: "success",
117
+ is_error: false,
118
+ result: "Final answer",
119
+ session_id: SESSION_ID,
120
+ usage: {
121
+ input_tokens: 600,
122
+ output_tokens: 280,
123
+ cache_read_input_tokens: 30,
124
+ cache_creation_input_tokens: 35,
125
+ },
126
+ total_cost_usd: 0.0579,
127
+ modelUsage: {
128
+ [SONNET]: {
129
+ inputTokens: 300,
130
+ outputTokens: 130,
131
+ cacheReadInputTokens: 30,
132
+ cacheCreationInputTokens: 5,
133
+ costUSD: 0.0123,
134
+ webSearchRequests: 0,
135
+ contextWindow: 200000,
136
+ maxOutputTokens: 8192,
137
+ },
138
+ [OPUS]: {
139
+ inputTokens: 300,
140
+ outputTokens: 150,
141
+ cacheReadInputTokens: 0,
142
+ cacheCreationInputTokens: 30,
143
+ costUSD: 0.0456,
144
+ webSearchRequests: 0,
145
+ contextWindow: 200000,
146
+ maxOutputTokens: 8192,
147
+ },
148
+ },
149
+ }),
150
+ ];
151
+
152
+ /**
153
+ * The authoritative return the parser must reproduce byte-identically on a
154
+ * clean exit — sourced from the terminal `result` event, NOT the running
155
+ * accumulator.
156
+ */
157
+ export const expected = {
158
+ result: "Final answer",
159
+ sessionId: SESSION_ID,
160
+ totalCostUsd: 0.0579,
161
+ modelUsage: {
162
+ [SONNET]: {
163
+ inputTokens: 300,
164
+ outputTokens: 130,
165
+ cacheReadInputTokens: 30,
166
+ cacheCreationInputTokens: 5,
167
+ costUSD: 0.0123,
168
+ webSearchRequests: 0,
169
+ contextWindow: 200000,
170
+ maxOutputTokens: 8192,
171
+ },
172
+ [OPUS]: {
173
+ inputTokens: 300,
174
+ outputTokens: 150,
175
+ cacheReadInputTokens: 0,
176
+ cacheCreationInputTokens: 30,
177
+ costUSD: 0.0456,
178
+ webSearchRequests: 0,
179
+ contextWindow: 200000,
180
+ maxOutputTokens: 8192,
181
+ },
182
+ },
183
+ };
184
+
185
+ /**
186
+ * The running per-model total the streaming parser accumulates from the
187
+ * `assistant` lines alone (deduped by message.id). costUSD is 0 because cost is
188
+ * not known until the terminal `result` event. This is what the final
189
+ * `onProgress` callback should receive.
190
+ */
191
+ export const expectedAccumulated = {
192
+ [SONNET]: {
193
+ inputTokens: 300, // 100 (msg_1, counted once) + 200 (msg_2)
194
+ outputTokens: 130, // 50 + 80
195
+ cacheReadInputTokens: 30, // 10 + 20
196
+ cacheCreationInputTokens: 5, // 5 + 0
197
+ costUSD: 0,
198
+ },
199
+ [OPUS]: {
200
+ inputTokens: 300,
201
+ outputTokens: 150,
202
+ cacheReadInputTokens: 0,
203
+ cacheCreationInputTokens: 30,
204
+ costUSD: 0,
205
+ },
206
+ };
207
+
208
+ /** Distinct message ids in emission order — onProgress should fire once each. */
209
+ export const expectedProgressCount = 3;
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Hand-authored synthetic `claude -p --output-format stream-json --verbose`
3
+ * transcript exercising `agent/src/runtime/progress-milestones.ts`'s block → phase
4
+ * mapping end-to-end through `_consumeStream`'s `handleLine` closure.
5
+ *
6
+ * Shape of the session:
7
+ * - one `system`/`init` line
8
+ * - assistant turn `msg_1`: a `thinking` block, then a `tool_use` Read block
9
+ * (no `usage` field on this line — a tool_use-only line must still be able
10
+ * to fire a phase update, exercised BEFORE the usage guard)
11
+ * - assistant turn `msg_2`: a second `tool_use` Read block (same phase as
12
+ * msg_1's Read — repeated identical phases must collapse to a single fire)
13
+ * - assistant turn `msg_3`: an `mcp__*`-prefixed tool name → generic `tool`
14
+ * - assistant turn `msg_4`: an unrecognized/unknown tool name → generic `tool`
15
+ * - assistant turn `msg_5`: a plain `text` block → `writing`
16
+ * - one terminal `result`/success line
17
+ *
18
+ * `tool_use` blocks intentionally carry `input` payloads containing
19
+ * fake-secret-shaped values and absolute filesystem paths — the mapper must
20
+ * never read `block.input`, so these values must never surface anywhere in
21
+ * the resulting phase sequence.
22
+ */
23
+
24
+ const SONNET = "claude-sonnet-4-6";
25
+ const SESSION_ID = "sess-progress-phases";
26
+
27
+ export const lines: string[] = [
28
+ JSON.stringify({
29
+ type: "system",
30
+ subtype: "init",
31
+ session_id: SESSION_ID,
32
+ model: SONNET,
33
+ tools: ["Read", "Grep", "Bash"],
34
+ }),
35
+ // msg_1 — thinking block, then a tool_use Read block. No `usage` field:
36
+ // exercises firing a phase BEFORE the existing usage guard.
37
+ JSON.stringify({
38
+ type: "assistant",
39
+ message: {
40
+ id: "msg_1",
41
+ role: "assistant",
42
+ model: SONNET,
43
+ content: [
44
+ { type: "thinking", thinking: "Let me look at the file first." },
45
+ {
46
+ type: "tool_use",
47
+ id: "tu_1",
48
+ name: "Read",
49
+ input: {
50
+ file_path: "/home/alice/.ssh/id_rsa",
51
+ api_key: "sk-fake-12345",
52
+ },
53
+ },
54
+ ],
55
+ },
56
+ session_id: SESSION_ID,
57
+ }),
58
+ // tool-result feedback — out of scope for phase mapping
59
+ JSON.stringify({
60
+ type: "user",
61
+ message: {
62
+ role: "user",
63
+ content: [{ type: "tool_result", tool_use_id: "tu_1", content: "ok" }],
64
+ },
65
+ session_id: SESSION_ID,
66
+ }),
67
+ // msg_2 — a SECOND Read tool_use block: same phase ("reading") as msg_1's
68
+ // Read block — must collapse to a single fire (no new phase event).
69
+ JSON.stringify({
70
+ type: "assistant",
71
+ message: {
72
+ id: "msg_2",
73
+ role: "assistant",
74
+ model: SONNET,
75
+ content: [
76
+ {
77
+ type: "tool_use",
78
+ id: "tu_2",
79
+ name: "Read",
80
+ input: {
81
+ command: "cat /home/alice/.ssh/id_rsa",
82
+ api_key: "sk-fake-12345",
83
+ },
84
+ },
85
+ ],
86
+ usage: {
87
+ input_tokens: 50,
88
+ output_tokens: 10,
89
+ cache_read_input_tokens: 0,
90
+ cache_creation_input_tokens: 0,
91
+ },
92
+ },
93
+ session_id: SESSION_ID,
94
+ }),
95
+ // msg_3 — an mcp__*-prefixed tool name → generic "tool" phase.
96
+ JSON.stringify({
97
+ type: "assistant",
98
+ message: {
99
+ id: "msg_3",
100
+ role: "assistant",
101
+ model: SONNET,
102
+ content: [
103
+ {
104
+ type: "tool_use",
105
+ id: "tu_3",
106
+ name: "mcp__linear__create_issue",
107
+ input: { api_key: "sk-fake-12345" },
108
+ },
109
+ ],
110
+ usage: {
111
+ input_tokens: 20,
112
+ output_tokens: 5,
113
+ cache_read_input_tokens: 0,
114
+ cache_creation_input_tokens: 0,
115
+ },
116
+ },
117
+ session_id: SESSION_ID,
118
+ }),
119
+ // msg_4 — an unrecognized/unknown tool name → generic "tool" phase.
120
+ JSON.stringify({
121
+ type: "assistant",
122
+ message: {
123
+ id: "msg_4",
124
+ role: "assistant",
125
+ model: SONNET,
126
+ content: [
127
+ {
128
+ type: "tool_use",
129
+ id: "tu_4",
130
+ name: "SomeFutureTool",
131
+ input: { secret: "sk-fake-12345" },
132
+ },
133
+ ],
134
+ usage: {
135
+ input_tokens: 20,
136
+ output_tokens: 5,
137
+ cache_read_input_tokens: 0,
138
+ cache_creation_input_tokens: 0,
139
+ },
140
+ },
141
+ session_id: SESSION_ID,
142
+ }),
143
+ // msg_5 — plain text block → "writing".
144
+ JSON.stringify({
145
+ type: "assistant",
146
+ message: {
147
+ id: "msg_5",
148
+ role: "assistant",
149
+ model: SONNET,
150
+ content: [{ type: "text", text: "Here's what I found." }],
151
+ usage: {
152
+ input_tokens: 30,
153
+ output_tokens: 15,
154
+ cache_read_input_tokens: 0,
155
+ cache_creation_input_tokens: 0,
156
+ },
157
+ },
158
+ session_id: SESSION_ID,
159
+ }),
160
+ // terminal result
161
+ JSON.stringify({
162
+ type: "result",
163
+ subtype: "success",
164
+ is_error: false,
165
+ result: "Here's what I found.",
166
+ session_id: SESSION_ID,
167
+ usage: {
168
+ input_tokens: 120,
169
+ output_tokens: 35,
170
+ cache_read_input_tokens: 0,
171
+ cache_creation_input_tokens: 0,
172
+ },
173
+ total_cost_usd: 0.0042,
174
+ modelUsage: {
175
+ [SONNET]: {
176
+ inputTokens: 120,
177
+ outputTokens: 35,
178
+ cacheReadInputTokens: 0,
179
+ cacheCreationInputTokens: 0,
180
+ costUSD: 0.0042,
181
+ },
182
+ },
183
+ }),
184
+ ];
185
+
186
+ /**
187
+ * The ordered sequence of DISTINCT phase fires `onProgress`/`perCallOnProgress`
188
+ * should emit across this fixture (collapsed — a phase identical to the
189
+ * immediately preceding fired phase does not fire again). `msg_1`'s thinking
190
+ * block fires "thinking" first, then its Read tool_use fires "reading".
191
+ * `msg_2`'s Read tool_use maps to the same "reading" phase as msg_1's —
192
+ * collapsed, no second fire. `msg_3` (mcp__*) maps to the generic "tool"
193
+ * phase — a new fire (differs from "reading"). `msg_4` (unknown tool) ALSO
194
+ * maps to "tool" — identical to the immediately preceding fired phase, so it
195
+ * collapses too (no second "tool" fire). `msg_5`'s text block fires "writing".
196
+ */
197
+ export const expectedPhaseSequence = ["thinking", "reading", "tool", "writing"];
198
+
199
+ /**
200
+ * Fake-secret-shaped substrings embedded in this fixture's `tool_use.input`
201
+ * payloads — must never appear anywhere in the phase output.
202
+ */
203
+ export const forbiddenSubstrings = ["/home/alice/.ssh/id_rsa", "sk-fake-12345"];