@stigmer/runner 3.39.1-dev.20260928194002 → 3.40.1
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/README.md +1 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
- package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
- package/dist/activities/execute-deep-agent/turn-setup.js +35 -29
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/shared/prompt-sections.d.ts +6 -28
- package/dist/shared/prompt-sections.js +6 -35
- package/dist/shared/prompt-sections.js.map +1 -1
- package/dist/shared/skill-resolver.d.ts +2 -3
- package/dist/shared/skill-resolver.js +2 -3
- package/dist/shared/skill-resolver.js.map +1 -1
- package/dist/shared/workspace/sources/git.js +13 -6
- package/dist/shared/workspace/sources/git.js.map +1 -1
- package/package.json +5 -5
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
- package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
- package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
- package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
- package/src/activities/execute-deep-agent/turn-setup.ts +36 -31
- package/src/shared/__tests__/prompt-sections.test.ts +2 -54
- package/src/shared/prompt-sections.ts +6 -55
- package/src/shared/skill-resolver.ts +2 -3
- package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
- package/src/shared/workspace/sources/git.ts +14 -8
- package/dist/shared/skill-relevance.d.ts +0 -65
- package/dist/shared/skill-relevance.js +0 -175
- package/dist/shared/skill-relevance.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
- package/src/shared/__tests__/skill-relevance.test.ts +0 -292
- package/src/shared/skill-relevance.ts +0 -222
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
3
|
-
import { buildEnhancedSystemPrompt, composeUserMessage } from "../prompt-builder.js";
|
|
3
|
+
import { buildEnhancedSystemPrompt, composeUserMessage, type TurnMessageInput } from "../prompt-builder.js";
|
|
4
4
|
import { PLAN_MODE_DIRECTIVE } from "../../../shared/plan-mode-prompt.js";
|
|
5
5
|
import type { ProvisionResult } from "../../../shared/workspace/types.js";
|
|
6
6
|
|
|
@@ -22,9 +22,6 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
22
22
|
provisionResults: [],
|
|
23
23
|
containerRoot: "",
|
|
24
24
|
skillsPromptSection: "",
|
|
25
|
-
workspaceFileRefs: [],
|
|
26
|
-
workspaceRoot: "/workspace",
|
|
27
|
-
inputFiles: [],
|
|
28
25
|
});
|
|
29
26
|
|
|
30
27
|
expect(prompt.startsWith("You are a helpful assistant.")).toBe(true);
|
|
@@ -36,9 +33,6 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
36
33
|
provisionResults: [],
|
|
37
34
|
containerRoot: "",
|
|
38
35
|
skillsPromptSection: "",
|
|
39
|
-
workspaceFileRefs: [],
|
|
40
|
-
workspaceRoot: "/workspace",
|
|
41
|
-
inputFiles: [],
|
|
42
36
|
});
|
|
43
37
|
|
|
44
38
|
expect(prompt).toContain("## Response rules");
|
|
@@ -50,9 +44,6 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
50
44
|
provisionResults: [],
|
|
51
45
|
containerRoot: "",
|
|
52
46
|
skillsPromptSection: "",
|
|
53
|
-
workspaceFileRefs: [],
|
|
54
|
-
workspaceRoot: "/workspace",
|
|
55
|
-
inputFiles: [],
|
|
56
47
|
});
|
|
57
48
|
|
|
58
49
|
expect(prompt).toContain("## Working with tools");
|
|
@@ -66,9 +57,6 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
66
57
|
provisionResults: [],
|
|
67
58
|
containerRoot: "",
|
|
68
59
|
skillsPromptSection: "",
|
|
69
|
-
workspaceFileRefs: [],
|
|
70
|
-
workspaceRoot: "/workspace",
|
|
71
|
-
inputFiles: [],
|
|
72
60
|
interactionMode: InteractionMode.PLAN,
|
|
73
61
|
});
|
|
74
62
|
|
|
@@ -82,9 +70,6 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
82
70
|
provisionResults: [makeProvisionResult()],
|
|
83
71
|
containerRoot: "/workspace",
|
|
84
72
|
skillsPromptSection: "",
|
|
85
|
-
workspaceFileRefs: [],
|
|
86
|
-
workspaceRoot: "/workspace",
|
|
87
|
-
inputFiles: [],
|
|
88
73
|
});
|
|
89
74
|
|
|
90
75
|
expect(prompt).toContain("## Workspace");
|
|
@@ -100,9 +85,6 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
100
85
|
],
|
|
101
86
|
containerRoot: "/workspace",
|
|
102
87
|
skillsPromptSection: "",
|
|
103
|
-
workspaceFileRefs: [],
|
|
104
|
-
workspaceRoot: "/workspace",
|
|
105
|
-
inputFiles: [],
|
|
106
88
|
});
|
|
107
89
|
|
|
108
90
|
expect(prompt).toContain("2 workspace entries");
|
|
@@ -118,9 +100,6 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
118
100
|
})],
|
|
119
101
|
containerRoot: "/workspace",
|
|
120
102
|
skillsPromptSection: "",
|
|
121
|
-
workspaceFileRefs: [],
|
|
122
|
-
workspaceRoot: "/workspace",
|
|
123
|
-
inputFiles: [],
|
|
124
103
|
});
|
|
125
104
|
|
|
126
105
|
expect(prompt).toContain("src/");
|
|
@@ -133,151 +112,12 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
133
112
|
provisionResults: [],
|
|
134
113
|
containerRoot: "",
|
|
135
114
|
skillsPromptSection: "\n\n## Skills\n\n- coding-standards",
|
|
136
|
-
workspaceFileRefs: [],
|
|
137
|
-
workspaceRoot: "/workspace",
|
|
138
|
-
inputFiles: [],
|
|
139
115
|
});
|
|
140
116
|
|
|
141
117
|
expect(prompt).toContain("## Skills");
|
|
142
118
|
expect(prompt).toContain("coding-standards");
|
|
143
119
|
});
|
|
144
120
|
|
|
145
|
-
it("includes referenced files section", () => {
|
|
146
|
-
const prompt = buildEnhancedSystemPrompt({
|
|
147
|
-
instructions: "Test",
|
|
148
|
-
provisionResults: [],
|
|
149
|
-
containerRoot: "",
|
|
150
|
-
skillsPromptSection: "",
|
|
151
|
-
workspaceFileRefs: ["src/config.yaml", "README.md"],
|
|
152
|
-
workspaceRoot: "/workspace",
|
|
153
|
-
inputFiles: [],
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
expect(prompt).toContain("## Referenced Files");
|
|
157
|
-
expect(prompt).toContain("`src/config.yaml`");
|
|
158
|
-
expect(prompt).toContain("`README.md`");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("includes injected files section with size info", () => {
|
|
162
|
-
// The section renders the injector's own result type (sizeBytes). A local
|
|
163
|
-
// structural twin with a `size` field used to live in the prompt builder,
|
|
164
|
-
// and production wiring silently dropped the size — this pin holds the
|
|
165
|
-
// real field name so that drift class cannot return.
|
|
166
|
-
const prompt = buildEnhancedSystemPrompt({
|
|
167
|
-
instructions: "Test",
|
|
168
|
-
provisionResults: [],
|
|
169
|
-
containerRoot: "",
|
|
170
|
-
skillsPromptSection: "",
|
|
171
|
-
workspaceFileRefs: [],
|
|
172
|
-
workspaceRoot: "/workspace",
|
|
173
|
-
inputFiles: [
|
|
174
|
-
{ filename: "data.csv", relativePath: ".stigmer/inputs/data.csv", sizeBytes: 1024 },
|
|
175
|
-
{ filename: "notes.md", relativePath: ".stigmer/inputs/notes.md", sizeBytes: 12 },
|
|
176
|
-
],
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
expect(prompt).toContain("## Input Files");
|
|
180
|
-
expect(prompt).toContain("`.stigmer/inputs/data.csv` (1024 bytes)");
|
|
181
|
-
expect(prompt).toContain("`.stigmer/inputs/notes.md` (12 bytes)");
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("discloses a duplicate-renamed injected file's original name", () => {
|
|
185
|
-
const prompt = buildEnhancedSystemPrompt({
|
|
186
|
-
instructions: "Test",
|
|
187
|
-
provisionResults: [],
|
|
188
|
-
containerRoot: "",
|
|
189
|
-
skillsPromptSection: "",
|
|
190
|
-
workspaceFileRefs: [],
|
|
191
|
-
workspaceRoot: "/workspace",
|
|
192
|
-
inputFiles: [
|
|
193
|
-
{ filename: "report.pdf", relativePath: ".stigmer/inputs/report.pdf", sizeBytes: 10 },
|
|
194
|
-
{
|
|
195
|
-
filename: "report-2.pdf",
|
|
196
|
-
relativePath: ".stigmer/inputs/report-2.pdf",
|
|
197
|
-
sizeBytes: 20,
|
|
198
|
-
renamedFrom: "report.pdf",
|
|
199
|
-
},
|
|
200
|
-
],
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
expect(prompt).toContain(
|
|
204
|
-
"`.stigmer/inputs/report-2.pdf` (20 bytes) (renamed from duplicate 'report.pdf')",
|
|
205
|
-
);
|
|
206
|
-
// The first file keeps a clean entry — no disclosure noise.
|
|
207
|
-
expect(prompt).toContain("`.stigmer/inputs/report.pdf` (10 bytes)\n");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("lists a minted download URL beside its file with the presigned hand-off line (issue #532)", () => {
|
|
211
|
-
const prompt = buildEnhancedSystemPrompt({
|
|
212
|
-
instructions: "Test",
|
|
213
|
-
provisionResults: [],
|
|
214
|
-
containerRoot: "",
|
|
215
|
-
skillsPromptSection: "",
|
|
216
|
-
workspaceFileRefs: [],
|
|
217
|
-
workspaceRoot: "/workspace",
|
|
218
|
-
inputFiles: [
|
|
219
|
-
{
|
|
220
|
-
filename: "lease.pdf",
|
|
221
|
-
relativePath: ".stigmer/inputs/lease.pdf",
|
|
222
|
-
sizeBytes: 2048,
|
|
223
|
-
downloadUrl: "https://r2.example/lease?sig=abc",
|
|
224
|
-
},
|
|
225
|
-
{ filename: "notes.md", relativePath: ".stigmer/inputs/notes.md", sizeBytes: 12 },
|
|
226
|
-
],
|
|
227
|
-
downloadUrlKind: "presigned",
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
expect(prompt).toContain(
|
|
231
|
-
"`.stigmer/inputs/lease.pdf` (2048 bytes) — download URL: https://r2.example/lease?sig=abc",
|
|
232
|
-
);
|
|
233
|
-
// The URL-less file keeps a clean entry.
|
|
234
|
-
expect(prompt).toContain("`.stigmer/inputs/notes.md` (12 bytes)\n");
|
|
235
|
-
expect(prompt).toContain("These URLs are time-limited");
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("words a local-serve URL honestly — reachable only from this machine", () => {
|
|
239
|
-
const prompt = buildEnhancedSystemPrompt({
|
|
240
|
-
instructions: "Test",
|
|
241
|
-
provisionResults: [],
|
|
242
|
-
containerRoot: "",
|
|
243
|
-
skillsPromptSection: "",
|
|
244
|
-
workspaceFileRefs: [],
|
|
245
|
-
workspaceRoot: "/workspace",
|
|
246
|
-
inputFiles: [
|
|
247
|
-
{
|
|
248
|
-
filename: "lease.pdf",
|
|
249
|
-
relativePath: ".stigmer/inputs/lease.pdf",
|
|
250
|
-
sizeBytes: 2048,
|
|
251
|
-
downloadUrl: "http://localhost:7235/attachments/01A/lease.pdf",
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
downloadUrlKind: "local-serve",
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
expect(prompt).toContain("download URL: http://localhost:7235/attachments/01A/lease.pdf");
|
|
258
|
-
expect(prompt).toContain("reachable only from this machine");
|
|
259
|
-
expect(prompt).not.toContain("time-limited");
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("renders no hand-off line when no listed file carries a URL (kind alone is not enough)", () => {
|
|
263
|
-
const prompt = buildEnhancedSystemPrompt({
|
|
264
|
-
instructions: "Test",
|
|
265
|
-
provisionResults: [],
|
|
266
|
-
containerRoot: "",
|
|
267
|
-
skillsPromptSection: "",
|
|
268
|
-
workspaceFileRefs: [],
|
|
269
|
-
workspaceRoot: "/workspace",
|
|
270
|
-
inputFiles: [
|
|
271
|
-
{ filename: "local.csv", relativePath: ".stigmer/inputs/local.csv", sizeBytes: 5 },
|
|
272
|
-
],
|
|
273
|
-
downloadUrlKind: "presigned",
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
expect(prompt).toContain("## Input Files");
|
|
277
|
-
expect(prompt).not.toContain("download URL");
|
|
278
|
-
expect(prompt).not.toContain("time-limited");
|
|
279
|
-
});
|
|
280
|
-
|
|
281
121
|
it("produces a git repo description for git workspace entries", () => {
|
|
282
122
|
const prompt = buildEnhancedSystemPrompt({
|
|
283
123
|
instructions: "Test",
|
|
@@ -301,13 +141,10 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
301
141
|
],
|
|
302
142
|
containerRoot: "/workspace",
|
|
303
143
|
skillsPromptSection: "",
|
|
304
|
-
workspaceFileRefs: [],
|
|
305
|
-
workspaceRoot: "/workspace",
|
|
306
|
-
inputFiles: [],
|
|
307
144
|
});
|
|
308
145
|
|
|
309
|
-
expect(prompt).toContain("https://github.com/org/my-repo");
|
|
310
|
-
expect(prompt).toContain("abc1234");
|
|
146
|
+
expect(prompt).toContain("https://github.com/org/my-repo (branch: main)");
|
|
147
|
+
expect(prompt, "no commit: the clone's HEAD moves with every write-back commit").not.toContain("abc1234");
|
|
311
148
|
expect(prompt).toContain("empty workspace");
|
|
312
149
|
});
|
|
313
150
|
|
|
@@ -317,9 +154,6 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
317
154
|
provisionResults: [],
|
|
318
155
|
containerRoot: "",
|
|
319
156
|
skillsPromptSection: "",
|
|
320
|
-
workspaceFileRefs: [],
|
|
321
|
-
workspaceRoot: "/workspace",
|
|
322
|
-
inputFiles: [],
|
|
323
157
|
});
|
|
324
158
|
|
|
325
159
|
expect(prompt).not.toContain("## Workspace");
|
|
@@ -331,9 +165,7 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
331
165
|
provisionResults: [],
|
|
332
166
|
containerRoot: "",
|
|
333
167
|
skillsPromptSection: "",
|
|
334
|
-
workspaceFileRefs: [],
|
|
335
168
|
workspaceRoot: "",
|
|
336
|
-
inputFiles: [],
|
|
337
169
|
};
|
|
338
170
|
|
|
339
171
|
it("appends the bridge as standing session context (every-turn injection)", () => {
|
|
@@ -360,9 +192,7 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
360
192
|
provisionResults: [],
|
|
361
193
|
containerRoot: "",
|
|
362
194
|
skillsPromptSection: "",
|
|
363
|
-
workspaceFileRefs: [],
|
|
364
195
|
workspaceRoot: "",
|
|
365
|
-
inputFiles: [],
|
|
366
196
|
};
|
|
367
197
|
|
|
368
198
|
it("appends the sender as standing session context (every-turn injection)", () => {
|
|
@@ -389,9 +219,7 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
389
219
|
provisionResults: [],
|
|
390
220
|
containerRoot: "",
|
|
391
221
|
skillsPromptSection: "",
|
|
392
|
-
workspaceFileRefs: [],
|
|
393
222
|
workspaceRoot: "",
|
|
394
|
-
inputFiles: [],
|
|
395
223
|
};
|
|
396
224
|
|
|
397
225
|
it("appends the context as standing session context (every-turn injection)", () => {
|
|
@@ -434,9 +262,7 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
434
262
|
provisionResults: [],
|
|
435
263
|
containerRoot: "",
|
|
436
264
|
skillsPromptSection: "",
|
|
437
|
-
workspaceFileRefs: [],
|
|
438
265
|
workspaceRoot: "",
|
|
439
|
-
inputFiles: [],
|
|
440
266
|
};
|
|
441
267
|
|
|
442
268
|
it("appends the preferences with per-scope attribution (every-turn injection)", () => {
|
|
@@ -486,9 +312,7 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
486
312
|
provisionResults: [],
|
|
487
313
|
containerRoot: "",
|
|
488
314
|
skillsPromptSection: "",
|
|
489
|
-
workspaceFileRefs: [],
|
|
490
315
|
workspaceRoot: "",
|
|
491
|
-
inputFiles: [],
|
|
492
316
|
};
|
|
493
317
|
|
|
494
318
|
it("appends the confirmed facts with the defensive framing (every-turn injection)", () => {
|
|
@@ -534,9 +358,6 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
534
358
|
provisionResults: [],
|
|
535
359
|
containerRoot: "",
|
|
536
360
|
skillsPromptSection: "",
|
|
537
|
-
workspaceFileRefs: [],
|
|
538
|
-
workspaceRoot: "/workspace",
|
|
539
|
-
inputFiles: [],
|
|
540
361
|
};
|
|
541
362
|
|
|
542
363
|
it("appends the shared plan-mode directive plus the native-only read-boundary line as the final section", () => {
|
|
@@ -576,21 +397,14 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
576
397
|
provisionResults: [],
|
|
577
398
|
containerRoot: "",
|
|
578
399
|
skillsPromptSection: "",
|
|
579
|
-
workspaceFileRefs: [],
|
|
580
|
-
workspaceRoot: "/workspace",
|
|
581
|
-
inputFiles: [],
|
|
582
|
-
};
|
|
583
|
-
const planFile = {
|
|
584
|
-
filename: "plan.md",
|
|
585
|
-
relativePath: ".stigmer/inputs/plan.md",
|
|
586
|
-
sizeBytes: 1024,
|
|
587
400
|
};
|
|
401
|
+
const planPath = ".stigmer/inputs/plan.md";
|
|
588
402
|
|
|
589
403
|
it("appends the implement-plan directive pointing at the injected plan", () => {
|
|
590
404
|
const prompt = buildEnhancedSystemPrompt({
|
|
591
405
|
...base,
|
|
592
406
|
buildFromPlan: true,
|
|
593
|
-
|
|
407
|
+
approvedPlanPath: planPath,
|
|
594
408
|
});
|
|
595
409
|
|
|
596
410
|
expect(prompt).toContain("## Implement the approved plan");
|
|
@@ -612,7 +426,7 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
612
426
|
it("omits the directive for an ordinary execution", () => {
|
|
613
427
|
const prompt = buildEnhancedSystemPrompt({
|
|
614
428
|
...base,
|
|
615
|
-
|
|
429
|
+
approvedPlanPath: planPath,
|
|
616
430
|
});
|
|
617
431
|
|
|
618
432
|
expect(prompt).not.toContain("## Implement the approved plan");
|
|
@@ -622,7 +436,7 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
622
436
|
const prompt = buildEnhancedSystemPrompt({
|
|
623
437
|
...base,
|
|
624
438
|
buildFromPlan: true,
|
|
625
|
-
|
|
439
|
+
approvedPlanPath: planPath,
|
|
626
440
|
});
|
|
627
441
|
|
|
628
442
|
expect(prompt).toContain("to-do list");
|
|
@@ -635,9 +449,11 @@ describe("composeUserMessage (conversation catchup, cloud DD-006 / A27)", () =>
|
|
|
635
449
|
const MESSAGE = "where is my order?";
|
|
636
450
|
const DIGEST =
|
|
637
451
|
"Customer: I want a refund\nTeammate: I've refunded you in full.";
|
|
452
|
+
const withCatchup = (conversationCatchup: string | undefined): string =>
|
|
453
|
+
composeUserMessage({ message: MESSAGE, inputFiles: [], workspaceFileRefs: [], ...(conversationCatchup !== undefined ? { conversationCatchup } : {}) });
|
|
638
454
|
|
|
639
455
|
it("prepends the framed catchup to the turn's user message — history durability rides the checkpointer, not the rebuilt system prompt", () => {
|
|
640
|
-
const composed =
|
|
456
|
+
const composed = withCatchup(DIGEST);
|
|
641
457
|
|
|
642
458
|
expect(composed.endsWith(MESSAGE)).toBe(true);
|
|
643
459
|
expect(composed).toContain(DIGEST);
|
|
@@ -646,11 +462,11 @@ describe("composeUserMessage (conversation catchup, cloud DD-006 / A27)", () =>
|
|
|
646
462
|
});
|
|
647
463
|
|
|
648
464
|
it("separates the catchup from the customer's message with a horizontal rule", () => {
|
|
649
|
-
expect(
|
|
465
|
+
expect(withCatchup(DIGEST)).toContain("\n\n---\n\n");
|
|
650
466
|
});
|
|
651
467
|
|
|
652
468
|
it("leaves the message untouched when there is no catchup — most turns carry none", () => {
|
|
653
|
-
expect(
|
|
469
|
+
expect(withCatchup(undefined)).toBe(MESSAGE);
|
|
654
470
|
});
|
|
655
471
|
|
|
656
472
|
it("never renders in the system prompt — the rebuilt-per-invocation lane would forget the digest one turn later", () => {
|
|
@@ -659,12 +475,124 @@ describe("composeUserMessage (conversation catchup, cloud DD-006 / A27)", () =>
|
|
|
659
475
|
provisionResults: [],
|
|
660
476
|
containerRoot: "",
|
|
661
477
|
skillsPromptSection: "",
|
|
662
|
-
workspaceFileRefs: [],
|
|
663
|
-
workspaceRoot: "/workspace",
|
|
664
|
-
inputFiles: [],
|
|
665
478
|
});
|
|
666
479
|
|
|
667
480
|
expect(prompt).not.toContain("Conversation catchup");
|
|
668
481
|
expect(prompt).not.toContain("you have not seen");
|
|
669
482
|
});
|
|
670
483
|
});
|
|
484
|
+
|
|
485
|
+
describe("composeUserMessage (the turn's payload rides the turn's message)", () => {
|
|
486
|
+
const MESSAGE = "Review the attached report.";
|
|
487
|
+
const message = (overrides: Partial<TurnMessageInput>): string =>
|
|
488
|
+
composeUserMessage({ message: MESSAGE, inputFiles: [], workspaceFileRefs: [], ...overrides });
|
|
489
|
+
|
|
490
|
+
it("names the referenced workspace paths, one bullet each, before the message", () => {
|
|
491
|
+
const composed = message({ workspaceFileRefs: ["src/config.yaml", "README.md"] });
|
|
492
|
+
|
|
493
|
+
expect(composed).toContain("## Referenced Files");
|
|
494
|
+
expect(composed).toContain("- `src/config.yaml`\n- `README.md`");
|
|
495
|
+
expect(composed.endsWith(`\n\n---\n\n${MESSAGE}`)).toBe(true);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("lists the input files with their size", () => {
|
|
499
|
+
// The section renders the injector's own result type (sizeBytes). A local
|
|
500
|
+
// structural twin with a `size` field used to live in the prompt builder,
|
|
501
|
+
// and production wiring silently dropped the size — this pin holds the
|
|
502
|
+
// real field name so that drift class cannot return.
|
|
503
|
+
const composed = message({
|
|
504
|
+
inputFiles: [
|
|
505
|
+
{ filename: "data.csv", relativePath: ".stigmer/inputs/data.csv", sizeBytes: 1024 },
|
|
506
|
+
{ filename: "notes.md", relativePath: ".stigmer/inputs/notes.md", sizeBytes: 12 },
|
|
507
|
+
],
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
expect(composed).toContain("## Input Files");
|
|
511
|
+
expect(composed).toContain("`.stigmer/inputs/data.csv` (1024 bytes)");
|
|
512
|
+
expect(composed).toContain("`.stigmer/inputs/notes.md` (12 bytes)");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("discloses a duplicate-renamed input file's original name", () => {
|
|
516
|
+
const composed = message({
|
|
517
|
+
inputFiles: [
|
|
518
|
+
{ filename: "report.pdf", relativePath: ".stigmer/inputs/report.pdf", sizeBytes: 10 },
|
|
519
|
+
{ filename: "report-2.pdf", relativePath: ".stigmer/inputs/report-2.pdf", sizeBytes: 20, renamedFrom: "report.pdf" },
|
|
520
|
+
],
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
expect(composed).toContain("`.stigmer/inputs/report-2.pdf` (20 bytes) (renamed from duplicate 'report.pdf')");
|
|
524
|
+
// The first file keeps a clean entry — no disclosure noise.
|
|
525
|
+
expect(composed).toContain("`.stigmer/inputs/report.pdf` (10 bytes)\n");
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("lists a minted download URL beside its file with the presigned hand-off line (issue #532)", () => {
|
|
529
|
+
const composed = message({
|
|
530
|
+
inputFiles: [
|
|
531
|
+
{ filename: "lease.pdf", relativePath: ".stigmer/inputs/lease.pdf", sizeBytes: 2048, downloadUrl: "https://r2.example/lease?sig=abc" },
|
|
532
|
+
{ filename: "notes.md", relativePath: ".stigmer/inputs/notes.md", sizeBytes: 12 },
|
|
533
|
+
],
|
|
534
|
+
downloadUrlKind: "presigned",
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
expect(composed).toContain("`.stigmer/inputs/lease.pdf` (2048 bytes) — download URL: https://r2.example/lease?sig=abc");
|
|
538
|
+
// The URL-less file keeps a clean entry.
|
|
539
|
+
expect(composed).toContain("`.stigmer/inputs/notes.md` (12 bytes)\n");
|
|
540
|
+
expect(composed).toContain("These URLs are time-limited");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("words a local-serve URL honestly — reachable only from this machine", () => {
|
|
544
|
+
const composed = message({
|
|
545
|
+
inputFiles: [
|
|
546
|
+
{
|
|
547
|
+
filename: "lease.pdf",
|
|
548
|
+
relativePath: ".stigmer/inputs/lease.pdf",
|
|
549
|
+
sizeBytes: 2048,
|
|
550
|
+
downloadUrl: "http://localhost:7235/attachments/01A/lease.pdf",
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
downloadUrlKind: "local-serve",
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
expect(composed).toContain("download URL: http://localhost:7235/attachments/01A/lease.pdf");
|
|
557
|
+
expect(composed).toContain("reachable only from this machine");
|
|
558
|
+
expect(composed).not.toContain("time-limited");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("renders no hand-off line when no listed file carries a URL (kind alone is not enough)", () => {
|
|
562
|
+
const composed = message({
|
|
563
|
+
inputFiles: [{ filename: "local.csv", relativePath: ".stigmer/inputs/local.csv", sizeBytes: 5 }],
|
|
564
|
+
downloadUrlKind: "presigned",
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
expect(composed).toContain("## Input Files");
|
|
568
|
+
expect(composed).not.toContain("download URL");
|
|
569
|
+
expect(composed).not.toContain("time-limited");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("orders the payload first, the catchup next and what the user typed last, each set off by a rule", () => {
|
|
573
|
+
const composed = message({
|
|
574
|
+
inputFiles: [{ filename: "a.txt", relativePath: ".stigmer/inputs/a.txt", sizeBytes: 1 }],
|
|
575
|
+
workspaceFileRefs: ["src/a.ts"],
|
|
576
|
+
conversationCatchup: "Customer: hello",
|
|
577
|
+
});
|
|
578
|
+
const parts = composed.split("\n\n---\n\n");
|
|
579
|
+
|
|
580
|
+
expect(parts).toHaveLength(4);
|
|
581
|
+
expect(parts[0]).toMatch(/^## Input Files/);
|
|
582
|
+
expect(parts[1]).toMatch(/^## Referenced Files/);
|
|
583
|
+
expect(parts[2]).toContain("Customer: hello");
|
|
584
|
+
expect(parts[3]).toBe(MESSAGE);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("never renders the payload in the system prompt, which is the session's", () => {
|
|
588
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
589
|
+
instructions: "Test",
|
|
590
|
+
provisionResults: [],
|
|
591
|
+
containerRoot: "",
|
|
592
|
+
skillsPromptSection: "",
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(prompt).not.toContain("## Input Files");
|
|
596
|
+
expect(prompt).not.toContain("## Referenced Files");
|
|
597
|
+
});
|
|
598
|
+
});
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The native system prompt, whole, as goldens.
|
|
2
|
+
* The native system prompt and the turn's user message, whole, as goldens.
|
|
3
3
|
*
|
|
4
4
|
* Every other prompt test in this directory asserts with `toContain`; the
|
|
5
5
|
* hermetic goldens are status JSON and never see a prompt. So until #1096 no
|
|
6
|
-
* test pinned a rendered prompt byte for byte, and the
|
|
7
|
-
*
|
|
8
|
-
* `renderRootSkillsSection`) had no test at all. These goldens are the
|
|
6
|
+
* test pinned a rendered prompt byte for byte, and the `## Skills` section at
|
|
7
|
+
* eight or more skills had no test at all. These goldens are the
|
|
9
8
|
* photograph taken BEFORE the shared prompt glue moved into
|
|
10
9
|
* `shared/prompt-sections.ts` (#1096), so the move could be proven
|
|
11
10
|
* byte-identical — the same discipline the hermetic net applied to the
|
|
@@ -16,17 +15,17 @@
|
|
|
16
15
|
* `systemPromptOf`), so a changed byte can silently re-route a scripted reply
|
|
17
16
|
* in the sub-agent delegation golden.
|
|
18
17
|
*
|
|
19
|
-
* The shapes: everything at once (the multi-entry workspace, nine skills
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* and
|
|
24
|
-
* mode and build-from-plan are mutually exclusive in
|
|
25
|
-
* its own golden.
|
|
18
|
+
* The shapes: everything at once (the multi-entry workspace, nine skills,
|
|
19
|
+
* channel templates, all five standing sections); plan mode; build-from-plan;
|
|
20
|
+
* the minimal prompt (no instructions, so the built-in assistant's); and the
|
|
21
|
+
* turn's user message for the everything shape (its input files with a
|
|
22
|
+
* rename and a download URL, the vision disclosure, its referenced files and
|
|
23
|
+
* a catchup). Plan mode and build-from-plan are mutually exclusive in
|
|
24
|
+
* production, so each has its own golden.
|
|
26
25
|
*
|
|
27
|
-
* The goldens are taken through `turn-setup.ts` `composeSystemPrompt
|
|
28
|
-
* production
|
|
29
|
-
* input, so they pin the
|
|
26
|
+
* The goldens are taken through `turn-setup.ts` `composeSystemPrompt` and
|
|
27
|
+
* `composeTurnMessage`, the production mappings from the runtime's resolved
|
|
28
|
+
* record to the builder's input, so they pin the mappings too, not copies. (The first cut
|
|
30
29
|
* photographed the prompt through a field-for-field copy of the mapping as
|
|
31
30
|
* it then sat inline in `buildEngine`; the next extracted it and re-took the
|
|
32
31
|
* goldens through the
|
|
@@ -43,6 +42,12 @@
|
|
|
43
42
|
* a local-path entry names no host path; the multi-entry path sentence
|
|
44
43
|
* names the virtual root; an entry's tree and the Input Files section end
|
|
45
44
|
* with one blank line before the next heading.
|
|
45
|
+
* - 2026-09-29 (the native system prompt is the session's, never the turn's):
|
|
46
|
+
* every mounted skill is described whatever the count, so the relevance
|
|
47
|
+
* filter's `### Also Available` note and the below-threshold golden are
|
|
48
|
+
* gone; the Referenced Files and Input Files sections leave the system
|
|
49
|
+
* prompt for the turn's message, which has its own golden; a cloned entry
|
|
50
|
+
* names its branch and no commit.
|
|
46
51
|
*/
|
|
47
52
|
|
|
48
53
|
import { describe, it, expect } from "vitest";
|
|
@@ -62,7 +67,7 @@ import type { SkillMetadata } from "../../../shared/skill-resolver.js";
|
|
|
62
67
|
import type { RecalledMemoriesContent } from "../../../shared/recalled-memories.js";
|
|
63
68
|
import type { ProvisionResult } from "../../../shared/workspace/types.js";
|
|
64
69
|
import { buildEnhancedSystemPrompt } from "../prompt-builder.js";
|
|
65
|
-
import { composeSystemPrompt } from "../turn-setup.js";
|
|
70
|
+
import { composeSystemPrompt, composeTurnMessage } from "../turn-setup.js";
|
|
66
71
|
|
|
67
72
|
// ---------------------------------------------------------------------------
|
|
68
73
|
// Fixtures — every value is a plain fact a golden can name.
|
|
@@ -74,7 +79,7 @@ function skill(name: string, description: string): SkillMetadata {
|
|
|
74
79
|
return { name, description, path: `.stigmer/skills/${name}/SKILL.md` };
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
/** Nine skills
|
|
82
|
+
/** Nine skills, past the eight at which the native harness once described only those the message reached: every one is described. */
|
|
78
83
|
const NINE_SKILLS: readonly SkillMetadata[] = [
|
|
79
84
|
skill("k8s-deploy", "Deploy services to kubernetes clusters with helm charts"),
|
|
80
85
|
skill("release-notes", "Draft release notes from the merged pull requests"),
|
|
@@ -152,6 +157,7 @@ interface EverythingShape {
|
|
|
152
157
|
readonly interactionMode?: InteractionMode;
|
|
153
158
|
readonly buildFromPlan?: boolean;
|
|
154
159
|
readonly inputFiles?: readonly ResolvedAttachment[];
|
|
160
|
+
readonly conversationCatchup?: string;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
/**
|
|
@@ -205,7 +211,7 @@ function everythingInput(shape: EverythingShape): TurnInput {
|
|
|
205
211
|
senderIdentity: { value: "15550001111", kind: "whatsapp_phone" },
|
|
206
212
|
sessionContext: "The user is the on-call engineer this week.",
|
|
207
213
|
declaredPreferences: { orgContext: "We deploy to eu-west-1.", userContext: "Keep answers terse." },
|
|
208
|
-
conversationCatchup:
|
|
214
|
+
conversationCatchup: shape.conversationCatchup,
|
|
209
215
|
selectRecalledMemories: async () => RECALLED,
|
|
210
216
|
},
|
|
211
217
|
});
|
|
@@ -221,16 +227,11 @@ async function systemPromptOf(input: TurnInput): Promise<string> {
|
|
|
221
227
|
// ---------------------------------------------------------------------------
|
|
222
228
|
|
|
223
229
|
describe("native system prompt goldens", () => {
|
|
224
|
-
it("everything at once, nine skills
|
|
230
|
+
it("everything at once, nine skills, every one described", async () => {
|
|
225
231
|
const prompt = await systemPromptOf(everythingInput({ skills: NINE_SKILLS }));
|
|
226
232
|
await expect(prompt).toMatchFileSnapshot("./goldens/system-prompt.everything.prompt.md");
|
|
227
233
|
});
|
|
228
234
|
|
|
229
|
-
it("three skills, below the relevance threshold: every skill listed, no also-available note", async () => {
|
|
230
|
-
const prompt = await systemPromptOf(everythingInput({ skills: THREE_SKILLS }));
|
|
231
|
-
await expect(prompt).toMatchFileSnapshot("./goldens/system-prompt.skills-below-threshold.prompt.md");
|
|
232
|
-
});
|
|
233
|
-
|
|
234
235
|
it("plan mode appends the shared directive plus the native read-boundary sentence, last", async () => {
|
|
235
236
|
const prompt = await systemPromptOf(everythingInput({ skills: THREE_SKILLS, interactionMode: InteractionMode.PLAN }));
|
|
236
237
|
await expect(prompt).toMatchFileSnapshot("./goldens/system-prompt.plan-mode.prompt.md");
|
|
@@ -249,10 +250,14 @@ describe("native system prompt goldens", () => {
|
|
|
249
250
|
provisionResults: [],
|
|
250
251
|
containerRoot: "",
|
|
251
252
|
skillsPromptSection: "",
|
|
252
|
-
workspaceFileRefs: [],
|
|
253
|
-
workspaceRoot: "/ws",
|
|
254
|
-
inputFiles: [],
|
|
255
253
|
});
|
|
256
254
|
await expect(prompt).toMatchFileSnapshot("./goldens/system-prompt.minimal.prompt.md");
|
|
257
255
|
});
|
|
256
|
+
|
|
257
|
+
it("the turn's user message: the payload first, then the catchup, then what the user typed", async () => {
|
|
258
|
+
const message = composeTurnMessage(
|
|
259
|
+
everythingInput({ skills: NINE_SKILLS, conversationCatchup: "Customer: is the staging deploy done?\nTeammate: Yes, it went out at noon." }),
|
|
260
|
+
);
|
|
261
|
+
await expect(message).toMatchFileSnapshot("./goldens/user-message.everything.md");
|
|
262
|
+
});
|
|
258
263
|
});
|