@bastani/atomic 0.5.21 → 0.5.22-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +0 -12
- package/dist/commands/cli/claude-stop-hook.d.ts +65 -0
- package/dist/commands/cli/claude-stop-hook.d.ts.map +1 -0
- package/dist/sdk/providers/claude.d.ts +132 -84
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +4 -4
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/workflows/index.d.ts +1 -1
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/cli/claude-stop-hook.test.ts +155 -24
- package/src/commands/cli/claude-stop-hook.ts +122 -16
- package/src/commands/cli/workflow.ts +10 -0
- package/src/sdk/providers/claude.ts +511 -290
- package/src/sdk/runtime/executor.test.ts +173 -27
- package/src/sdk/runtime/executor.ts +348 -102
- package/src/sdk/types.ts +2 -4
- package/src/sdk/workflows/index.ts +0 -1
|
@@ -112,9 +112,13 @@ describe("renderMessagesToText", () => {
|
|
|
112
112
|
|
|
113
113
|
// --- Copilot ---
|
|
114
114
|
|
|
115
|
-
test("
|
|
116
|
-
const messages: SavedMessage[] = [
|
|
117
|
-
|
|
115
|
+
test("renders a copilot assistant.message under an Assistant header", () => {
|
|
116
|
+
const messages: SavedMessage[] = [
|
|
117
|
+
makeCopilotAssistantEvent("Hello from Copilot"),
|
|
118
|
+
];
|
|
119
|
+
expect(renderMessagesToText(messages)).toBe(
|
|
120
|
+
"### Assistant\n\nHello from Copilot",
|
|
121
|
+
);
|
|
118
122
|
});
|
|
119
123
|
|
|
120
124
|
test("skips copilot non-assistant events (session.start)", () => {
|
|
@@ -122,26 +126,30 @@ describe("renderMessagesToText", () => {
|
|
|
122
126
|
expect(renderMessagesToText(messages)).toBe("");
|
|
123
127
|
});
|
|
124
128
|
|
|
125
|
-
test("only
|
|
129
|
+
test("only renders copilot assistant.message events when mixed with other event types", () => {
|
|
126
130
|
const messages: SavedMessage[] = [
|
|
127
131
|
makeCopilotSessionStartEvent(),
|
|
128
132
|
makeCopilotAssistantEvent("First response"),
|
|
129
133
|
makeCopilotSessionStartEvent(),
|
|
130
134
|
makeCopilotAssistantEvent("Second response"),
|
|
131
135
|
];
|
|
132
|
-
expect(renderMessagesToText(messages)).toBe(
|
|
136
|
+
expect(renderMessagesToText(messages)).toBe(
|
|
137
|
+
"### Assistant\n\nFirst response\n\n### Assistant\n\nSecond response",
|
|
138
|
+
);
|
|
133
139
|
});
|
|
134
140
|
|
|
135
141
|
// --- OpenCode ---
|
|
136
142
|
|
|
137
|
-
test("
|
|
143
|
+
test("renders opencode text parts under an Assistant header", () => {
|
|
138
144
|
const messages: SavedMessage[] = [
|
|
139
145
|
makeOpenCodeMessage([
|
|
140
146
|
{ type: "text", text: "Line one" },
|
|
141
147
|
{ type: "text", text: "Line two" },
|
|
142
148
|
]),
|
|
143
149
|
];
|
|
144
|
-
expect(renderMessagesToText(messages)).toBe(
|
|
150
|
+
expect(renderMessagesToText(messages)).toBe(
|
|
151
|
+
"### Assistant\n\nLine one\n\nLine two",
|
|
152
|
+
);
|
|
145
153
|
});
|
|
146
154
|
|
|
147
155
|
test("filters out non-text parts from opencode messages", () => {
|
|
@@ -162,24 +170,32 @@ describe("renderMessagesToText", () => {
|
|
|
162
170
|
{ type: "subtask", text: "" },
|
|
163
171
|
]),
|
|
164
172
|
];
|
|
165
|
-
expect(renderMessagesToText(messages)).toBe(
|
|
173
|
+
expect(renderMessagesToText(messages)).toBe(
|
|
174
|
+
"### Assistant\n\nThe answer is 42",
|
|
175
|
+
);
|
|
166
176
|
});
|
|
167
177
|
|
|
168
178
|
// --- Claude ---
|
|
169
179
|
|
|
170
|
-
test("
|
|
171
|
-
const messages: SavedMessage[] = [
|
|
172
|
-
|
|
180
|
+
test("renders a claude assistant string message under an Assistant header", () => {
|
|
181
|
+
const messages: SavedMessage[] = [
|
|
182
|
+
makeClaudeMessage("assistant", "Plain string output"),
|
|
183
|
+
];
|
|
184
|
+
expect(renderMessagesToText(messages)).toBe(
|
|
185
|
+
"### Assistant\n\nPlain string output",
|
|
186
|
+
);
|
|
173
187
|
});
|
|
174
188
|
|
|
175
|
-
test("
|
|
189
|
+
test("renders claude assistant message with content as string under an Assistant header", () => {
|
|
176
190
|
const messages: SavedMessage[] = [
|
|
177
191
|
makeClaudeMessage("assistant", { content: "Content field string" }),
|
|
178
192
|
];
|
|
179
|
-
expect(renderMessagesToText(messages)).toBe(
|
|
193
|
+
expect(renderMessagesToText(messages)).toBe(
|
|
194
|
+
"### Assistant\n\nContent field string",
|
|
195
|
+
);
|
|
180
196
|
});
|
|
181
197
|
|
|
182
|
-
test("joins text blocks
|
|
198
|
+
test("joins claude text blocks with a double newline under a single Assistant header", () => {
|
|
183
199
|
const messages: SavedMessage[] = [
|
|
184
200
|
makeClaudeMessage("assistant", {
|
|
185
201
|
content: [
|
|
@@ -188,48 +204,176 @@ describe("renderMessagesToText", () => {
|
|
|
188
204
|
],
|
|
189
205
|
}),
|
|
190
206
|
];
|
|
191
|
-
expect(renderMessagesToText(messages)).toBe(
|
|
207
|
+
expect(renderMessagesToText(messages)).toBe(
|
|
208
|
+
"### Assistant\n\nBlock one\n\nBlock two",
|
|
209
|
+
);
|
|
192
210
|
});
|
|
193
211
|
|
|
194
|
-
test("
|
|
195
|
-
const messages: SavedMessage[] = [
|
|
196
|
-
|
|
212
|
+
test("renders a claude user string message under a User header", () => {
|
|
213
|
+
const messages: SavedMessage[] = [
|
|
214
|
+
makeClaudeMessage("user", "user prompt"),
|
|
215
|
+
];
|
|
216
|
+
expect(renderMessagesToText(messages)).toBe("### User\n\nuser prompt");
|
|
197
217
|
});
|
|
198
218
|
|
|
199
219
|
test("skips claude system messages", () => {
|
|
200
|
-
const messages: SavedMessage[] = [
|
|
220
|
+
const messages: SavedMessage[] = [
|
|
221
|
+
makeClaudeMessage("system", "system instructions"),
|
|
222
|
+
];
|
|
201
223
|
expect(renderMessagesToText(messages)).toBe("");
|
|
202
224
|
});
|
|
203
225
|
|
|
204
|
-
test("returns empty string for claude assistant with unknown
|
|
226
|
+
test("returns empty string for a claude assistant message with an unknown content shape", () => {
|
|
205
227
|
const unknownMsg = { weird: "shape", count: 99 };
|
|
206
|
-
const messages: SavedMessage[] = [
|
|
228
|
+
const messages: SavedMessage[] = [
|
|
229
|
+
makeClaudeMessage("assistant", unknownMsg),
|
|
230
|
+
];
|
|
207
231
|
expect(renderMessagesToText(messages)).toBe("");
|
|
208
232
|
});
|
|
209
233
|
|
|
210
|
-
test("
|
|
234
|
+
test("renders tool_use blocks inline with text under a single Assistant header", () => {
|
|
211
235
|
const messages: SavedMessage[] = [
|
|
212
236
|
makeClaudeMessage("assistant", {
|
|
213
237
|
content: [
|
|
214
238
|
{ type: "text", text: "I'll read the file" },
|
|
215
|
-
{
|
|
239
|
+
{
|
|
240
|
+
type: "tool_use",
|
|
241
|
+
id: "tu-1",
|
|
242
|
+
name: "Read",
|
|
243
|
+
input: { path: "/tmp/foo" },
|
|
244
|
+
},
|
|
216
245
|
{ type: "text", text: "Here's what I found" },
|
|
217
246
|
],
|
|
218
247
|
}),
|
|
219
248
|
];
|
|
220
|
-
expect(renderMessagesToText(messages)).toBe(
|
|
249
|
+
expect(renderMessagesToText(messages)).toBe(
|
|
250
|
+
[
|
|
251
|
+
"### Assistant",
|
|
252
|
+
"",
|
|
253
|
+
"I'll read the file",
|
|
254
|
+
"",
|
|
255
|
+
"**→ `Read`**",
|
|
256
|
+
"",
|
|
257
|
+
"```json",
|
|
258
|
+
"{\n \"path\": \"/tmp/foo\"\n}",
|
|
259
|
+
"```",
|
|
260
|
+
"",
|
|
261
|
+
"Here's what I found",
|
|
262
|
+
].join("\n"),
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("skips claude `thinking` blocks in the rendered transcript", () => {
|
|
267
|
+
const messages: SavedMessage[] = [
|
|
268
|
+
makeClaudeMessage("assistant", {
|
|
269
|
+
content: [
|
|
270
|
+
{ type: "thinking", thinking: "internal reasoning…", signature: "sig" },
|
|
271
|
+
{ type: "text", text: "Public answer" },
|
|
272
|
+
],
|
|
273
|
+
}),
|
|
274
|
+
];
|
|
275
|
+
expect(renderMessagesToText(messages)).toBe(
|
|
276
|
+
"### Assistant\n\nPublic answer",
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("omits `tool_result` payloads entirely — only the call and subsequent assistant turns survive", () => {
|
|
281
|
+
const big = "x".repeat(5_000);
|
|
282
|
+
const messages: SavedMessage[] = [
|
|
283
|
+
makeClaudeMessage("assistant", {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: "tool_use",
|
|
287
|
+
id: "tu-42",
|
|
288
|
+
name: "Read",
|
|
289
|
+
input: { file_path: "/tmp/note.md" },
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
}),
|
|
293
|
+
makeClaudeMessage("user", {
|
|
294
|
+
content: [
|
|
295
|
+
{ type: "tool_result", tool_use_id: "tu-42", content: big },
|
|
296
|
+
],
|
|
297
|
+
}),
|
|
298
|
+
makeClaudeMessage("assistant", {
|
|
299
|
+
content: [{ type: "text", text: "Done." }],
|
|
300
|
+
}),
|
|
301
|
+
];
|
|
302
|
+
const rendered = renderMessagesToText(messages);
|
|
303
|
+
|
|
304
|
+
// The tool call itself is present, with its input JSON.
|
|
305
|
+
expect(rendered).toContain("**→ `Read`**");
|
|
306
|
+
expect(rendered).toContain("/tmp/note.md");
|
|
307
|
+
|
|
308
|
+
// The follow-up assistant turn is present.
|
|
309
|
+
expect(rendered).toContain("### Assistant\n\nDone.");
|
|
310
|
+
|
|
311
|
+
// The tool_result payload is completely absent — not truncated, not
|
|
312
|
+
// labelled, not present in any form. This is the context-rot guard:
|
|
313
|
+
// even a 5_000-char result must not leak into the transcript.
|
|
314
|
+
expect(rendered).not.toContain("xxxxx");
|
|
315
|
+
expect(rendered).not.toContain("← `Read` result");
|
|
316
|
+
expect(rendered).not.toContain("← `Read`");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("truncates very long tool_use `input` payloads with a `[+N chars]` suffix", () => {
|
|
320
|
+
const bigCommand = "echo " + "a".repeat(5_000);
|
|
321
|
+
const messages: SavedMessage[] = [
|
|
322
|
+
makeClaudeMessage("assistant", {
|
|
323
|
+
content: [
|
|
324
|
+
{
|
|
325
|
+
type: "tool_use",
|
|
326
|
+
id: "tu-big",
|
|
327
|
+
name: "Bash",
|
|
328
|
+
input: { command: bigCommand },
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
}),
|
|
332
|
+
];
|
|
333
|
+
const rendered = renderMessagesToText(messages);
|
|
334
|
+
expect(rendered).toContain("**→ `Bash`**");
|
|
335
|
+
// Input budget is 800 chars of JSON — the long command must be truncated.
|
|
336
|
+
expect(rendered).toContain("chars]");
|
|
337
|
+
expect(rendered.length).toBeLessThan(bigCommand.length);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("skips a user message whose only content is `tool_result` blocks", () => {
|
|
341
|
+
const messages: SavedMessage[] = [
|
|
342
|
+
makeClaudeMessage("user", {
|
|
343
|
+
content: [
|
|
344
|
+
{
|
|
345
|
+
type: "tool_result",
|
|
346
|
+
tool_use_id: "tu-a",
|
|
347
|
+
content: "would-be-noisy output",
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
}),
|
|
351
|
+
];
|
|
352
|
+
expect(renderMessagesToText(messages)).toBe("");
|
|
221
353
|
});
|
|
222
354
|
|
|
223
355
|
// --- Mixed providers ---
|
|
224
356
|
|
|
225
|
-
test("joins messages from mixed providers with double newlines", () => {
|
|
357
|
+
test("joins messages from mixed providers with double newlines and provider-appropriate headers", () => {
|
|
226
358
|
const messages: SavedMessage[] = [
|
|
227
359
|
makeCopilotAssistantEvent("Copilot says hello"),
|
|
228
360
|
makeOpenCodeMessage([{ type: "text", text: "OpenCode says hello" }]),
|
|
229
361
|
makeClaudeMessage("assistant", "Claude says hello"),
|
|
230
362
|
];
|
|
231
363
|
expect(renderMessagesToText(messages)).toBe(
|
|
232
|
-
|
|
364
|
+
[
|
|
365
|
+
"### Assistant",
|
|
366
|
+
"",
|
|
367
|
+
"Copilot says hello",
|
|
368
|
+
"",
|
|
369
|
+
"### Assistant",
|
|
370
|
+
"",
|
|
371
|
+
"OpenCode says hello",
|
|
372
|
+
"",
|
|
373
|
+
"### Assistant",
|
|
374
|
+
"",
|
|
375
|
+
"Claude says hello",
|
|
376
|
+
].join("\n"),
|
|
233
377
|
);
|
|
234
378
|
});
|
|
235
379
|
|
|
@@ -239,7 +383,9 @@ describe("renderMessagesToText", () => {
|
|
|
239
383
|
makeCopilotAssistantEvent("Only one has content"),
|
|
240
384
|
makeOpenCodeMessage([{ type: "reasoning", text: "ignored" }]),
|
|
241
385
|
];
|
|
242
|
-
expect(renderMessagesToText(messages)).toBe(
|
|
386
|
+
expect(renderMessagesToText(messages)).toBe(
|
|
387
|
+
"### Assistant\n\nOnly one has content",
|
|
388
|
+
);
|
|
243
389
|
});
|
|
244
390
|
});
|
|
245
391
|
|