@stigmer/runner 3.40.0 → 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.
Files changed (35) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
  3. package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
  4. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  5. package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
  6. package/dist/activities/execute-deep-agent/turn-setup.js +35 -29
  7. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  8. package/dist/shared/prompt-sections.d.ts +6 -28
  9. package/dist/shared/prompt-sections.js +6 -35
  10. package/dist/shared/prompt-sections.js.map +1 -1
  11. package/dist/shared/skill-resolver.d.ts +2 -3
  12. package/dist/shared/skill-resolver.js +2 -3
  13. package/dist/shared/skill-resolver.js.map +1 -1
  14. package/dist/shared/workspace/sources/git.js +13 -6
  15. package/dist/shared/workspace/sources/git.js.map +1 -1
  16. package/package.json +5 -5
  17. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
  18. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
  19. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
  20. package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
  21. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
  22. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
  23. package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
  24. package/src/activities/execute-deep-agent/turn-setup.ts +36 -31
  25. package/src/shared/__tests__/prompt-sections.test.ts +2 -54
  26. package/src/shared/prompt-sections.ts +6 -55
  27. package/src/shared/skill-resolver.ts +2 -3
  28. package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
  29. package/src/shared/workspace/sources/git.ts +14 -8
  30. package/dist/shared/skill-relevance.d.ts +0 -65
  31. package/dist/shared/skill-relevance.js +0 -175
  32. package/dist/shared/skill-relevance.js.map +0 -1
  33. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
  34. package/src/shared/__tests__/skill-relevance.test.ts +0 -292
  35. package/src/shared/skill-relevance.ts +0 -222
@@ -1,19 +1,32 @@
1
1
  /**
2
- * The native harness's system prompt: what `createDeepAgent` receives as
3
- * `systemPrompt`, rebuilt on EVERY invocation (never checkpointed with the
4
- * message history), so every standing fact is injected on every turn by
5
- * design — a first-turn-only injection would vanish from turn 2 onward.
2
+ * The native harness's prompt: the system prompt `createDeepAgent` receives
3
+ * as `systemPrompt`, and the turn's user message.
4
+ *
5
+ * The system prompt is rebuilt on EVERY invocation (never checkpointed with
6
+ * the message history), so every standing fact is injected on every turn by
7
+ * design — a first-turn-only injection would vanish from turn 2 onward. It is
8
+ * a function of the session and of the execution's settings (its mode), never
9
+ * of the turn. The provider caches tools, system and messages as one prefix,
10
+ * so a system byte that follows the message re-writes the cached prompt and
11
+ * the whole conversation after it, on every turn, at the cache-write price.
12
+ * What belongs to one turn — its input files, the workspace paths it
13
+ * references, the conversation catchup — rides that turn's user message
14
+ * (`composeUserMessage`), which enters the checkpointer with the turn and
15
+ * stays in history. That is the Cursor harness's resumed-turn placement
16
+ * (`execute-cursor/prompt-builder.ts` `buildPrompt`) and the runner's rule
17
+ * that user content goes in user messages, never the system prompt.
6
18
  *
7
19
  * Pure functions: no side effects, no I/O. `turn-setup.ts` gathers the
8
20
  * inputs from the runtime's resolved record and this module renders them.
9
21
  *
10
22
  * What is this harness's and what is shared (since #1096):
11
23
  * - Shared through `shared/prompt-sections.ts`: WHICH standing sections
12
- * render and in WHAT order (`standingContextSections`), the input-files
13
- * bullet and disclosure lines (`inputFileLines`), which skills a prompt
14
- * highlights (`selectSkillsForPrompt`, called from `turn-setup.ts`) and
15
- * the also-available sentence. Their WORDS come from each fact's own
16
- * module; this builder only frames them.
24
+ * render and in WHAT order (`standingContextSections`) and the
25
+ * input-files bullet and disclosure lines (`inputFileLines`). Their WORDS
26
+ * come from each fact's own module; this builder only frames them. Every
27
+ * mounted skill is described on every turn, as the Cursor harness does:
28
+ * choosing skills by the message made the system prompt follow the
29
+ * message.
17
30
  * - This harness's: the framing — `## Heading` markdown sections, the house
18
31
  * style for a system prompt — and every text that quotes this engine's
19
32
  * tools: the `## Skills` activation protocol, the response rules, the
@@ -24,7 +37,8 @@
24
37
  *
25
38
  * The rendered bytes are load-bearing for the hermetic tests: `ScriptedModel`
26
39
  * tells its scripted roles apart by the system prompt's text, so this
27
- * module's whole output is pinned by `__tests__/prompt-goldens.test.ts`.
40
+ * module's whole output — the system prompt and the turn's message — is
41
+ * pinned by `__tests__/prompt-goldens.test.ts`.
28
42
  */
29
43
 
30
44
  import { relative } from "node:path";
@@ -43,10 +57,7 @@ import {
43
57
  } from "../../shared/prompt-sections.js";
44
58
  import { effectiveInstructions } from "../../shared/builtin-assistant-prompt.js";
45
59
  import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
46
- import {
47
- buildImplementPlanDirective,
48
- findApprovedPlanPath,
49
- } from "../../shared/implement-plan-prompt.js";
60
+ import { buildImplementPlanDirective } from "../../shared/implement-plan-prompt.js";
50
61
  import type { ResolvedAttachment } from "../../shared/attachment-resolver.js";
51
62
  import type { SkillMetadata } from "../../shared/skill-resolver.js";
52
63
  import { STIGMER_LOCAL_STATE_DIR } from "../../shared/workspace/stigmer-link.js";
@@ -131,22 +142,6 @@ export interface PromptBuilderInput {
131
142
  * when the agent serves no proactive channel or nothing is sendable.
132
143
  */
133
144
  channelTemplatesPromptSection?: string;
134
- workspaceFileRefs: string[];
135
- workspaceRoot: string;
136
- /** The turn's resolved input files, as the runtime's attachment phase returns them. */
137
- inputFiles: readonly ResolvedAttachment[];
138
- /**
139
- * Vision facts about this turn's attachments (T04): which images the model
140
- * sees inline in the user message and which degraded to path-only.
141
- * Rendered inside the Input Files section.
142
- */
143
- vision?: VisionPromptInfo;
144
- /**
145
- * What kind of URL the turn's storage backend mints (issue #532) — keys
146
- * the Input Files section's hand-off wording (attachment-download-urls.ts).
147
- * One turn-level fact: all attachments ride the one configured storage.
148
- */
149
- downloadUrlKind?: DownloadUrlKind;
150
145
  /**
151
146
  * The execution's interaction mode. PLAN appends the shared plan-mode
152
147
  * directive so the model knows the turn's deliverable is a plan document.
@@ -163,6 +158,14 @@ export interface PromptBuilderInput {
163
158
  * message itself is just a short label ("Build from plan").
164
159
  */
165
160
  buildFromPlan?: boolean;
161
+ /**
162
+ * The approved plan's path among the turn's input files
163
+ * (`implement-plan-prompt.ts` `findApprovedPlanPath`), read only under
164
+ * `buildFromPlan`. Absent when the plan did not materialize, which selects
165
+ * the directive's conversation-plan variant. The Input Files section
166
+ * itself rides the turn's message (`composeUserMessage`).
167
+ */
168
+ approvedPlanPath?: string;
166
169
  /**
167
170
  * Rollover context bridge (cloud DD-013): a digest of the previous
168
171
  * session's conversation, read from `SessionSpec.metadata`. Injected on
@@ -252,22 +255,6 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
252
255
  prompt += "\n\n" + input.channelTemplatesPromptSection;
253
256
  }
254
257
 
255
- if (input.workspaceFileRefs.length > 0) {
256
- const refSection = buildReferencedFilesSection(
257
- input.workspaceFileRefs,
258
- input.workspaceRoot,
259
- );
260
- if (refSection) {
261
- prompt += refSection;
262
- }
263
- }
264
-
265
- if (input.inputFiles.length > 0) {
266
- prompt += buildInputFilesSection(
267
- input.inputFiles, input.vision, input.downloadUrlKind,
268
- );
269
- }
270
-
271
258
  // The standing context, in the shared order (the doctrine is stated once,
272
259
  // on `standingContextSections`); this harness only frames each section.
273
260
  for (const section of standingContextSections(input)) {
@@ -296,34 +283,67 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
296
283
  }
297
284
 
298
285
  if (input.buildFromPlan) {
299
- const planPath = findApprovedPlanPath(
300
- input.inputFiles.map((f) => f.relativePath),
301
- );
302
286
  prompt +=
303
287
  "\n\n## Implement the approved plan\n\n" +
304
- buildImplementPlanDirective(planPath);
288
+ buildImplementPlanDirective(input.approvedPlanPath);
305
289
  }
306
290
 
307
291
  return prompt;
308
292
  }
309
293
 
294
+ /** The turn's own content, composed around what the user typed. */
295
+ export interface TurnMessageInput {
296
+ /** What the user typed (`spec.message`); never mutated. */
297
+ readonly message: string;
298
+ /** The conversation catchup digest (cloud DD-006), when the turn carries one. */
299
+ readonly conversationCatchup?: string;
300
+ /** This turn's resolved input files, as the runtime's attachment phase returns them. */
301
+ readonly inputFiles: readonly ResolvedAttachment[];
302
+ /**
303
+ * Vision facts about this turn's attachments (T04): which images the model
304
+ * sees inline in this same message and which degraded to path-only.
305
+ * Rendered inside the Input Files section.
306
+ */
307
+ readonly vision?: VisionPromptInfo;
308
+ /**
309
+ * What kind of URL the turn's storage backend mints (issue #532) — keys
310
+ * the Input Files section's hand-off wording (attachment-download-urls.ts).
311
+ * One turn-level fact: all attachments ride the one configured storage.
312
+ */
313
+ readonly downloadUrlKind?: DownloadUrlKind;
314
+ /** The workspace paths the user highlighted for this message (`spec.workspace_file_refs`). */
315
+ readonly workspaceFileRefs: readonly string[];
316
+ }
317
+
310
318
  /**
311
- * Compose the turn's USER MESSAGE for the graph invocation: the framed
312
- * conversation catchup (cloud DD-006), when present, prepended to the
313
- * customer's message. In the user message and never the system prompt (A27):
314
- * the system prompt is rebuilt per invocation and would forget the digest one
315
- * turn later, while a message enters the checkpointer with the turn and
316
- * persists in history — the same durability the cursor harness gets from its
317
- * prompt prefix. The caller's `spec.message` is never mutated; the prepend
318
- * exists only in the graph input.
319
+ * Compose the turn's USER MESSAGE for the graph invocation: this turn's
320
+ * payload (its input files, then the workspace paths it references), then the
321
+ * framed conversation catchup (cloud DD-006), then what the user typed, each
322
+ * set off by a horizontal rule. Payload first and context closest to the task
323
+ * is the Cursor harness's resumed-turn order.
324
+ *
325
+ * In the user message and never the system prompt (A27, and the module
326
+ * header): the system prompt is rebuilt per invocation, so a turn's content
327
+ * there would vanish one turn later and, while it lasted, change the cached
328
+ * prefix; a message enters the checkpointer with the turn and persists in
329
+ * history, so a later turn still knows which files an earlier one carried.
330
+ * An approval resume sends no new message and needs none: the payload is
331
+ * already in the checkpointed one. The caller's `spec.message` is never
332
+ * mutated; the composition exists only in the graph input.
319
333
  */
320
- export function composeUserMessage(
321
- message: string,
322
- conversationCatchup: string | undefined,
323
- ): string {
324
- return conversationCatchup
325
- ? `${formatConversationCatchupText(conversationCatchup)}\n\n---\n\n${message}`
326
- : message;
334
+ export function composeUserMessage(input: TurnMessageInput): string {
335
+ const parts: string[] = [];
336
+ if (input.inputFiles.length > 0) {
337
+ parts.push(buildInputFilesSection(input.inputFiles, input.vision, input.downloadUrlKind));
338
+ }
339
+ if (input.workspaceFileRefs.length > 0) {
340
+ parts.push(buildReferencedFilesSection(input.workspaceFileRefs));
341
+ }
342
+ if (input.conversationCatchup) {
343
+ parts.push(formatConversationCatchupText(input.conversationCatchup));
344
+ }
345
+ parts.push(input.message);
346
+ return parts.join("\n\n---\n\n");
327
347
  }
328
348
 
329
349
  /**
@@ -454,14 +474,14 @@ function formatEntryDescription(result: ProvisionResult): string {
454
474
  );
455
475
  }
456
476
 
477
+ // No commit: the clone's HEAD moves with every write-back commit, and a
478
+ // system prompt that names it would change on every such turn
479
+ // (`shared/workspace/sources/git.ts` says the same for one entry).
457
480
  if (result.sourceType === "git_repo" && result.gitMetadata) {
458
481
  const meta: GitMetadata = result.gitMetadata;
459
- const shortSha = meta.baseCommit.length >= 7
460
- ? meta.baseCommit.slice(0, 7)
461
- : meta.baseCommit;
482
+ const branch = meta.branch ? ` (branch: ${meta.branch})` : "";
462
483
  return (
463
- `Workspace entry **${name}** was initialized from ` +
464
- `${meta.repoUrl} (branch: ${meta.branch}, commit: ${shortSha}).\n` +
484
+ `Workspace entry **${name}** was initialized from ${meta.repoUrl}${branch}.\n` +
465
485
  "Changes you make will be captured as artifacts when execution completes."
466
486
  );
467
487
  }
@@ -476,36 +496,29 @@ function formatEntryDescription(result: ProvisionResult): string {
476
496
  return result.workspaceDescription;
477
497
  }
478
498
 
479
- function buildReferencedFilesSection(
480
- workspaceFileRefs: string[],
481
- _workspaceRoot: string,
482
- ): string {
483
- if (workspaceFileRefs.length === 0) return "";
484
-
485
- let section =
486
- "\n\n## Referenced Files\n\n" +
499
+ /** The `## Referenced Files` part of the turn's message: the paths the user highlighted, one bullet each. */
500
+ function buildReferencedFilesSection(workspaceFileRefs: readonly string[]): string {
501
+ return (
502
+ "## Referenced Files\n\n" +
487
503
  "The user has highlighted the following workspace paths for your " +
488
- `attention. Use \`${ENGINE_TOOL.readFile}\` to access file contents.\n\n`;
489
-
490
- for (const refPath of workspaceFileRefs) {
491
- section += `- \`${refPath}\`\n`;
492
- }
493
-
494
- return section;
504
+ `attention. Use \`${ENGINE_TOOL.readFile}\` to access file contents.\n\n` +
505
+ workspaceFileRefs.map((refPath) => `- \`${refPath}\``).join("\n")
506
+ );
495
507
  }
496
508
 
497
509
  /**
498
- * The `## Input Files` section: this harness's intro (it names this engine's
499
- * file-reading tool) around the shared lines (`prompt-sections.ts` `inputFileLines`
500
- * says what each bullet and disclosure carries and why). Each disclosure
501
- * group is set off by a blank line, the markdown idiom.
510
+ * The `## Input Files` part of the turn's message: this harness's intro (it
511
+ * names this engine's file-reading tool) around the shared lines
512
+ * (`prompt-sections.ts` `inputFileLines` says what each bullet and disclosure
513
+ * carries and why). Each disclosure group is set off by a blank line, the
514
+ * markdown idiom.
502
515
  */
503
516
  function buildInputFilesSection(
504
517
  files: readonly ResolvedAttachment[],
505
518
  vision?: VisionPromptInfo,
506
519
  downloadUrlKind?: DownloadUrlKind,
507
520
  ): string {
508
- let section = "\n\n## Input Files\n\n";
521
+ let section = "## Input Files\n\n";
509
522
  section +=
510
523
  "The following files have been provided as read-only reference " +
511
524
  "material for your task. They live under `.stigmer/inputs/` and " +
@@ -526,7 +539,6 @@ function buildInputFilesSection(
526
539
  section += "\n" + lines.vision.join("\n") + "\n";
527
540
  }
528
541
 
529
- // Every section opens with its own blank line ("\n\n## ..."), so this one
530
- // ends on its last line rather than adding a second blank (#1127).
542
+ // The part ends on its last line; the message's rule sets it off (#1127).
531
543
  return section.replace(/\n+$/, "");
532
544
  }
@@ -67,7 +67,8 @@ import { buildChatModel } from "../../shared/model-client.js";
67
67
  import { graphThinks, toAnthropicThinking } from "../../shared/thinking-mode.js";
68
68
  import { isUnattendedApprovalMode, type MergedToolPolicy } from "../../shared/approval-policy.js";
69
69
  import type { ToolApprovalCategory } from "../../shared/tool-kind.js";
70
- import { alsoAvailableSkillsNote, selectSkillsForPrompt, visionPromptInfoOf } from "../../shared/prompt-sections.js";
70
+ import { visionPromptInfoOf } from "../../shared/prompt-sections.js";
71
+ import { findApprovedPlanPath } from "../../shared/implement-plan-prompt.js";
71
72
  import type { RecalledMemoriesContent } from "../../shared/recalled-memories.js";
72
73
  import { toLangChainImageBlocks } from "../../shared/attachment-vision.js";
73
74
  import { resolveRecursionLimit, UNBOUNDED_ADVISORY_RECURSION_LIMIT } from "../../shared/tool-rounds.js";
@@ -260,25 +261,6 @@ export function readGateState(input: TurnInput, tools: DeepAgentTools): DeepAgen
260
261
  };
261
262
  }
262
263
 
263
- /**
264
- * The `## Skills` section of the root prompt: the skills the shared selection
265
- * highlights for this turn's message (`shared/prompt-sections.ts`
266
- * `selectSkillsForPrompt`: every skill below the threshold, the relevant ones
267
- * at and above it), rendered with this engine's activation protocol, plus —
268
- * when the selection left any out — an `### Also Available` note naming them
269
- * (the sentence is the shared, tool-neutral one; the heading is this
270
- * harness's markdown framing).
271
- */
272
- export function renderRootSkillsSection(input: TurnInput): string {
273
- const { highlighted, alsoAvailable } = selectSkillsForPrompt(input.execution.spec?.message ?? "", input.skills.root);
274
- if (alsoAvailable.length === 0) return renderSkillsSection(highlighted);
275
- console.log(
276
- `[turn-setup] Skill relevance filter: ${highlighted.length} included, ` +
277
- `${alsoAvailable.length} excluded: ${alsoAvailable.join(", ")}`,
278
- );
279
- return renderSkillsSection(highlighted) + "\n### Also Available\n\n" + alsoAvailableSkillsNote(alsoAvailable) + "\n";
280
- }
281
-
282
264
  /**
283
265
  * The system prompt over the runtime's resolved record: every input the
284
266
  * builder renders is read from `TurnInput` here, in one place, so the mapping
@@ -287,6 +269,12 @@ export function renderRootSkillsSection(input: TurnInput): string {
287
269
  * that is not on the record is the memoized memory selection, which the
288
270
  * caller awaits (the runtime stamps the report on the status the first time
289
271
  * it is pulled, before this turn's first persist).
272
+ *
273
+ * Nothing here reads the turn's message or payload: the system prompt is the
274
+ * session's (`prompt-builder.ts` says why), so every mounted skill is
275
+ * described and the turn's files ride its message (`composeTurnMessage`).
276
+ * The approved plan's path is the one fact taken from the attachments, and
277
+ * only on a build-from-plan turn, where the mode already changes the prompt.
290
278
  */
291
279
  export function composeSystemPrompt(input: TurnInput, recalledMemories: RecalledMemoriesContent | undefined): string {
292
280
  const spec = input.execution.spec!;
@@ -296,19 +284,17 @@ export function composeSystemPrompt(input: TurnInput, recalledMemories: Recalled
296
284
  instructions: input.blueprint.instructions,
297
285
  provisionResults: input.workspace.provision.provisionResults,
298
286
  containerRoot: primaryDir,
299
- skillsPromptSection: renderRootSkillsSection(input),
287
+ skillsPromptSection: renderSkillsSection(input.skills.root),
300
288
  // "" (nothing sendable) threads as undefined: the tool alone still serves
301
289
  // text sends inside a 24-hour window (DD-006 D6).
302
290
  channelTemplatesPromptSection: input.mcp.channelMessaging.length > 0
303
291
  ? formatChannelTemplatesSection(input.mcp.channelMessaging) || undefined
304
292
  : undefined,
305
- workspaceFileRefs: spec.workspaceFileRefs ?? [],
306
- workspaceRoot: primaryDir,
307
- inputFiles: input.attachments.results,
308
- vision: visionPromptInfoOf(input.attachments),
309
- downloadUrlKind: input.artifactStorage?.downloadUrlKind,
310
293
  interactionMode: execConfig?.interactionMode,
311
294
  buildFromPlan: execConfig?.buildFromPlan,
295
+ ...(execConfig?.buildFromPlan
296
+ ? { approvedPlanPath: findApprovedPlanPath(input.attachments.results.map((f) => f.relativePath)) }
297
+ : {}),
312
298
  contextBridge: input.standing.contextBridge,
313
299
  senderIdentity: input.standing.senderIdentity,
314
300
  sessionContext: input.standing.sessionContext,
@@ -317,6 +303,25 @@ export function composeSystemPrompt(input: TurnInput, recalledMemories: Recalled
317
303
  });
318
304
  }
319
305
 
306
+ /**
307
+ * The turn's user message over the runtime's resolved record: what the user
308
+ * typed, with this turn's payload (its input files and referenced paths) and
309
+ * the conversation catchup composed around it (`prompt-builder.ts`
310
+ * `composeUserMessage`). The twin of `composeSystemPrompt` for the turn's
311
+ * half, so the goldens pin this mapping and not a copy of it.
312
+ */
313
+ export function composeTurnMessage(input: TurnInput): string {
314
+ const spec = input.execution.spec!;
315
+ return composeUserMessage({
316
+ message: spec.message,
317
+ conversationCatchup: input.standing.conversationCatchup,
318
+ inputFiles: input.attachments.results,
319
+ vision: visionPromptInfoOf(input.attachments),
320
+ downloadUrlKind: input.artifactStorage?.downloadUrlKind,
321
+ workspaceFileRefs: spec.workspaceFileRefs ?? [],
322
+ });
323
+ }
324
+
320
325
  /**
321
326
  * Build the engine: the model, the middleware stack with the gate, the
322
327
  * tools, the compiled sub-agents, the system prompt and `createDeepAgent`.
@@ -577,9 +582,10 @@ export async function buildEngine(
577
582
  * What the graph is invoked with. The checkpoint is read once (on the
578
583
  * durable http saver an extra `getState` is a network round-trip): pending
579
584
  * interrupts that the runtime's decisions answer make a `Command(resume)`;
580
- * anything else is the turn's user message — the conversation catchup
581
- * framed before it (cloud DD-006; in the USER MESSAGE so it enters the
582
- * checkpointer with the turn), the structured-output contract appended, and
585
+ * anything else is the turn's user message — this turn's payload and the
586
+ * conversation catchup framed before it (`composeTurnMessage`; in the USER
587
+ * MESSAGE so it enters the checkpointer with the turn and the system prompt
588
+ * stays the session's), the structured-output contract appended, and
583
589
  * the inline images FIRST as content blocks when the turn carries any (per
584
590
  * Anthropic's images-before-text guidance; a plain string otherwise).
585
591
  */
@@ -590,8 +596,7 @@ export async function composeGraphInput(input: TurnInput, engine: DeepAgentEngin
590
596
  return { kind: "resume", command: resume.graphInput };
591
597
  }
592
598
 
593
- const spec = input.execution.spec!;
594
- let userMessage = composeUserMessage(spec.message, input.standing.conversationCatchup);
599
+ let userMessage = composeTurnMessage(input);
595
600
  if (engine.hasStructuredOutput) {
596
601
  userMessage += `\n\n---\nIMPORTANT: When your analysis is complete, provide your findings as structured output matching the required schema. The system will capture your structured response automatically.`;
597
602
  }
@@ -1,8 +1,6 @@
1
1
  /**
2
- * The shared prompt glue (`shared/prompt-sections.ts`): the standing order,
3
- * the input-files lines, the skill selection and the also-available sentence
4
- * (the sentence's arms came from `skill-writer.test.ts`, retired with its
5
- * module in #1096). What each harness renders AROUND these is pinned whole by
2
+ * The shared prompt glue (`shared/prompt-sections.ts`): the standing order
3
+ * and the input-files lines. What each harness renders AROUND these is pinned whole by
6
4
  * the two builders' prompt goldens (`execute-deep-agent/__tests__/
7
5
  * prompt-goldens.test.ts`, `execute-cursor/__tests__/prompt-goldens.test.ts`);
8
6
  * this file pins the glue's own contract, harness-free.
@@ -10,16 +8,12 @@
10
8
 
11
9
  import { describe, it, expect } from "vitest";
12
10
  import {
13
- alsoAvailableSkillsNote,
14
11
  inputFileLines,
15
- selectSkillsForPrompt,
16
12
  standingContextSections,
17
13
  visionPromptInfoOf,
18
14
  type StandingSectionKind,
19
15
  } from "../prompt-sections.js";
20
16
  import type { ResolvedAttachment } from "../attachment-resolver.js";
21
- import type { SkillMetadata } from "../skill-resolver.js";
22
- import { SKILL_COUNT_THRESHOLD } from "../skill-relevance.js";
23
17
  import { formatDeclaredPreferencesText } from "../declared-preferences.js";
24
18
 
25
19
  describe("standingContextSections", () => {
@@ -119,49 +113,3 @@ describe("visionPromptInfoOf", () => {
119
113
  });
120
114
  });
121
115
  });
122
-
123
- describe("selectSkillsForPrompt", () => {
124
- const skill = (name: string, description: string): SkillMetadata => ({ name, description, path: `.stigmer/skills/${name}/SKILL.md` });
125
-
126
- it("highlights every skill below the threshold, in mount order, and names none as also-available", () => {
127
- const skills = [skill("alpha", "Deploy to kubernetes"), skill("beta", "Reshape CSV exports")];
128
- const selection = selectSkillsForPrompt("deploy the service", skills);
129
- expect(selection.highlighted).toEqual(skills);
130
- expect(selection.alsoAvailable).toEqual([]);
131
- });
132
-
133
- it("at the threshold, highlights the skills the message reaches and names the rest", () => {
134
- const skills = Array.from({ length: SKILL_COUNT_THRESHOLD }, (_, i) =>
135
- i === 0 ? skill("k8s-deploy", "Deploy services to kubernetes clusters") : skill(`other-${i}`, `Unrelated thing number ${i}`),
136
- );
137
- const selection = selectSkillsForPrompt("Deploy the payments service to kubernetes", skills);
138
- expect(selection.highlighted.map((s) => s.name)).toContain("k8s-deploy");
139
- expect(selection.alsoAvailable.length).toBeGreaterThan(0);
140
- expect(selection.highlighted.length + selection.alsoAvailable.length).toBe(SKILL_COUNT_THRESHOLD);
141
- });
142
-
143
- it("selects nothing from nothing", () => {
144
- expect(selectSkillsForPrompt("anything", [])).toEqual({ highlighted: [], alsoAvailable: [] });
145
- });
146
- });
147
-
148
- describe("alsoAvailableSkillsNote", () => {
149
- it("returns empty for no excluded names", () => {
150
- expect(alsoAvailableSkillsNote([])).toBe("");
151
- });
152
-
153
- it("lists excluded skills with backtick formatting", () => {
154
- const note = alsoAvailableSkillsNote(["alpha", "beta", "gamma"]);
155
- expect(note).toContain("`alpha`");
156
- expect(note).toContain("`beta`");
157
- expect(note).toContain("`gamma`");
158
- });
159
-
160
- it("includes activation instructions that name no tool, so any harness can carry it", () => {
161
- const note = alsoAvailableSkillsNote(["some-skill"]);
162
- expect(note).toContain(".stigmer/skills/<name>/SKILL.md");
163
- expect(note).toContain("relevant to your task");
164
- expect(note).not.toMatch(/\b(read|Read) tool\b/);
165
- expect(note.startsWith("#")).toBe(false);
166
- });
167
- });
@@ -9,8 +9,12 @@
9
9
  * `attachment-download-urls.ts`). What the two prompt builders duplicated
10
10
  * until #1096 was the glue around those bodies: the order
11
11
  * the five standing sections read in, with the same doctrine comments written
12
- * twice; the input-files bullet and disclosure lines; which skills a prompt
13
- * highlights when there are many. This module is that glue, written once.
12
+ * twice; the input-files bullet and disclosure lines. This module is that
13
+ * glue, written once. It also chose which skills a prompt highlighted when
14
+ * there were many, until the native harness stopped choosing skills by the
15
+ * turn's message: a system prompt that follows the message misses the
16
+ * provider's prompt cache on every turn, so every harness now describes every
17
+ * mounted skill.
14
18
  *
15
19
  * What stays with each harness, on purpose:
16
20
  * - Framing and placement. The native harness renders `## Heading` sections
@@ -43,9 +47,6 @@ import { formatDeclaredPreferencesText, type DeclaredPreferencesContent } from "
43
47
  import { formatRecalledMemoriesText, type RecalledMemoriesContent } from "./recalled-memories.js";
44
48
  import { formatSenderIdentityText, type SenderIdentity } from "./sender-identity.js";
45
49
  import { formatSessionContextText } from "./session-context.js";
46
- import { filterSkills } from "./skill-relevance.js";
47
- import type { SkillMetadata } from "./skill-resolver.js";
48
- import { STIGMER_LOCAL_STATE_DIR } from "./workspace/stigmer-link.js";
49
50
 
50
51
  // ---------------------------------------------------------------------------
51
52
  // The standing context: five sections, one order
@@ -204,53 +205,3 @@ export function inputFileLines(
204
205
  vision: vision ? visionDisclosureLines(vision.inlineFilenames, vision.notViewable) : [],
205
206
  };
206
207
  }
207
-
208
- // ---------------------------------------------------------------------------
209
- // The skills: which to highlight
210
- // ---------------------------------------------------------------------------
211
-
212
- /** Which mounted skills a prompt highlights, and which it only names. */
213
- export interface SkillSelection {
214
- /** The skills the prompt describes in full, in the filter's order. */
215
- readonly highlighted: readonly SkillMetadata[];
216
- /** The names of the skills left out of the highlighted set; empty below the threshold. */
217
- readonly alsoAvailable: readonly string[];
218
- }
219
-
220
- /**
221
- * The relevance selection over the turn's message (`skill-relevance.ts`):
222
- * below `SKILL_COUNT_THRESHOLD` every skill is highlighted; at and above it,
223
- * the skills the message's terms reach are highlighted and the rest are
224
- * named, so the agent can still activate one on its own judgement (the Agent
225
- * Skills spec's progressive disclosure: metadata up front, instructions on
226
- * demand). The native harness calls this on every turn; the Cursor harness
227
- * lists every skill today; adopting the selection there is an open item.
228
- */
229
- export function selectSkillsForPrompt(userMessage: string, skills: readonly SkillMetadata[]): SkillSelection {
230
- const filter = filterSkills(
231
- userMessage,
232
- skills.map((s) => s.name),
233
- skills.map((s) => s.description),
234
- );
235
- return {
236
- highlighted: filter.includedIndices.map((i) => skills[i]),
237
- alsoAvailable: filter.excludedNames,
238
- };
239
- }
240
-
241
- /**
242
- * The tool-neutral sentence that names the skills a prompt did not highlight
243
- * (`SkillSelection.alsoAvailable`), so the agent can activate one by reading
244
- * its SKILL.md at the mounted path. The sentence names no tool, so it serves
245
- * any harness; the heading above it is the builder's framing. Empty for no
246
- * names.
247
- */
248
- export function alsoAvailableSkillsNote(excludedNames: readonly string[]): string {
249
- if (excludedNames.length === 0) return "";
250
- const names = excludedNames.map((n) => `\`${n}\``).join(", ");
251
- return (
252
- `These skills are installed but were not highlighted above: ${names}. ` +
253
- "If you determine one of them is relevant to your task, " +
254
- `read its SKILL.md at \`${STIGMER_LOCAL_STATE_DIR}/skills/<name>/SKILL.md\` to activate it.`
255
- );
256
- }
@@ -14,9 +14,8 @@
14
14
  * into its prompt in its own placement. Moved from
15
15
  * `activities/execute-cursor/` in #1070; the native harness's twin
16
16
  * (`skill-writer.ts` `fetchSkillsByRefs` + `mountSkills`) retired in #1096,
17
- * and the module itself with it — the one renderer it kept is
18
- * `shared/prompt-sections.ts` `alsoAvailableSkillsNote`. This is the one
19
- * skills path for every harness.
17
+ * and the module itself with it. This is the one skills path for every
18
+ * harness.
20
19
  *
21
20
  * The mount is cached by the skill's content-addressed version hash
22
21
  * (stigmer/stigmer#672): metadata is fetched on every execution (that keeps
@@ -73,14 +73,17 @@ describe("provisionGit", () => {
73
73
  expect(cmds.some((c) => c.includes("git checkout 'develop'"))).toBe(true);
74
74
  });
75
75
 
76
- it("sets workspace description with repo URL and branch", async () => {
76
+ it("describes the clone by repo URL and branch, never by its commit", async () => {
77
77
  const backend = mockWorkspaceBackend({
78
78
  execute: routingExecute({ branch: "main\n", sha: "sha789\n" }),
79
79
  });
80
80
  const result = await provisionGit(makeOptions({ backend }));
81
81
  expect(result.workspaceDescription).toContain("github.com/org/repo.git");
82
- expect(result.workspaceDescription).toContain("main");
83
- expect(result.workspaceDescription).toContain("sha789");
82
+ expect(result.workspaceDescription).toContain("(branch: main)");
83
+ // The native system prompt carries this text on every turn; write-back
84
+ // commits move HEAD every approved turn, so a commit here would change it.
85
+ expect(result.workspaceDescription).not.toContain("sha789");
86
+ expect(result.gitMetadata?.baseCommit, "the commit stays in the metadata write-back reads").toBe("sha789");
84
87
  });
85
88
 
86
89
  // ── Default-branch resolution ─────────────────────────────────────
@@ -120,7 +123,10 @@ describe("provisionGit", () => {
120
123
 
121
124
  const result = await provisionGit(makeOptions({ backend }));
122
125
  expect(result.sourceType).toBe("git_repo");
123
- expect(result.workspaceDescription).toContain("existing repo detected");
126
+ const fresh = await provisionGit(
127
+ makeOptions({ backend: mockWorkspaceBackend({ execute: routingExecute() }) }),
128
+ );
129
+ expect(result.workspaceDescription, "a reused clone is described exactly as a fresh one").toBe(fresh.workspaceDescription);
124
130
 
125
131
  expect(calls(backend).some((c) => c.includes("git init"))).toBe(false);
126
132
  expect(calls(backend).some((c) => c.includes("git fetch"))).toBe(false);
@@ -395,7 +401,7 @@ describe("provisionGit", () => {
395
401
  }));
396
402
 
397
403
  expect(result.gitMetadata!.gitCredentialsConfigured).toBe(true);
398
- expect(result.workspaceDescription).toContain("existing repo detected");
404
+ expect(calls(backend).some((c) => c.includes("git init")), "the reuse path, not a fresh clone").toBe(false);
399
405
  });
400
406
 
401
407
  it("handles credential setup failure gracefully", async () => {