@stigmer/runner 3.1.2 → 3.1.4

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 (127) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/attachment-resolver.d.ts +1 -1
  3. package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
  4. package/dist/activities/execute-cursor/capture-flow.d.ts +29 -0
  5. package/dist/activities/execute-cursor/capture-flow.js +59 -0
  6. package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
  7. package/dist/activities/execute-cursor/command-provenance.d.ts +11 -25
  8. package/dist/activities/execute-cursor/command-provenance.js +25 -115
  9. package/dist/activities/execute-cursor/command-provenance.js.map +1 -1
  10. package/dist/activities/execute-cursor/index.js +358 -557
  11. package/dist/activities/execute-cursor/index.js.map +1 -1
  12. package/dist/activities/execute-cursor/todo-tracker.d.ts +6 -1
  13. package/dist/activities/execute-cursor/todo-tracker.js +15 -43
  14. package/dist/activities/execute-cursor/todo-tracker.js.map +1 -1
  15. package/dist/activities/execute-cursor/turn-boundary.d.ts +108 -0
  16. package/dist/activities/execute-cursor/turn-boundary.js +163 -0
  17. package/dist/activities/execute-cursor/turn-boundary.js.map +1 -0
  18. package/dist/activities/execute-cursor/turn-stream.d.ts +141 -0
  19. package/dist/activities/execute-cursor/turn-stream.js +249 -0
  20. package/dist/activities/execute-cursor/turn-stream.js.map +1 -0
  21. package/dist/activities/execute-deep-agent/command-provenance.d.ts +61 -0
  22. package/dist/activities/execute-deep-agent/command-provenance.js +72 -0
  23. package/dist/activities/execute-deep-agent/command-provenance.js.map +1 -0
  24. package/dist/activities/execute-deep-agent/index.js +73 -4
  25. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  26. package/dist/activities/execute-deep-agent/status-builder.js +8 -1
  27. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  28. package/dist/activities/execute-deep-agent/streaming-v3.js +6 -1
  29. package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/streaming.d.ts +7 -0
  31. package/dist/activities/execute-deep-agent/streaming.js +5 -1
  32. package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/v3-status-builder.js +12 -1
  34. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  35. package/dist/otel.js +10 -0
  36. package/dist/otel.js.map +1 -1
  37. package/dist/shared/filereview/capture.d.ts +18 -1
  38. package/dist/shared/filereview/capture.js +105 -28
  39. package/dist/shared/filereview/capture.js.map +1 -1
  40. package/dist/shared/filereview/cas-progress.d.ts +63 -0
  41. package/dist/shared/filereview/cas-progress.js +128 -0
  42. package/dist/shared/filereview/cas-progress.js.map +1 -0
  43. package/dist/shared/filereview/cas-substrate.d.ts +38 -0
  44. package/dist/shared/filereview/cas-substrate.js +48 -15
  45. package/dist/shared/filereview/cas-substrate.js.map +1 -1
  46. package/dist/shared/filereview/command-provenance.d.ts +93 -0
  47. package/dist/shared/filereview/command-provenance.js +132 -0
  48. package/dist/shared/filereview/command-provenance.js.map +1 -0
  49. package/dist/shared/filereview/events.d.ts +16 -0
  50. package/dist/shared/filereview/events.js +37 -0
  51. package/dist/shared/filereview/events.js.map +1 -1
  52. package/dist/shared/filereview/git-substrate.d.ts +48 -0
  53. package/dist/shared/filereview/git-substrate.js +78 -0
  54. package/dist/shared/filereview/git-substrate.js.map +1 -1
  55. package/dist/shared/filereview/index.d.ts +5 -2
  56. package/dist/shared/filereview/index.js +5 -2
  57. package/dist/shared/filereview/index.js.map +1 -1
  58. package/dist/shared/filereview/line-counts.d.ts +48 -0
  59. package/dist/shared/filereview/line-counts.js +72 -0
  60. package/dist/shared/filereview/line-counts.js.map +1 -0
  61. package/dist/shared/filereview/progress.d.ts +167 -0
  62. package/dist/shared/filereview/progress.js +196 -0
  63. package/dist/shared/filereview/progress.js.map +1 -0
  64. package/dist/shared/implement-plan-prompt.d.ts +6 -4
  65. package/dist/shared/implement-plan-prompt.js +38 -6
  66. package/dist/shared/implement-plan-prompt.js.map +1 -1
  67. package/dist/shared/plan-artifact.d.ts +59 -11
  68. package/dist/shared/plan-artifact.js +144 -16
  69. package/dist/shared/plan-artifact.js.map +1 -1
  70. package/dist/shared/plan-mode-prompt.d.ts +15 -4
  71. package/dist/shared/plan-mode-prompt.js +28 -5
  72. package/dist/shared/plan-mode-prompt.js.map +1 -1
  73. package/dist/shared/todos.d.ts +56 -0
  74. package/dist/shared/todos.js +98 -0
  75. package/dist/shared/todos.js.map +1 -0
  76. package/dist/shared/tool-row.d.ts +16 -0
  77. package/dist/shared/tool-row.js +31 -0
  78. package/dist/shared/tool-row.js.map +1 -1
  79. package/package.json +3 -2
  80. package/src/__tests__/otel-turn-span.test.ts +61 -0
  81. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -0
  82. package/src/activities/execute-cursor/__tests__/delta-enricher.test.ts +68 -0
  83. package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
  84. package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +280 -0
  85. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +349 -0
  86. package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
  87. package/src/activities/execute-cursor/capture-flow.ts +78 -0
  88. package/src/activities/execute-cursor/command-provenance.ts +25 -120
  89. package/src/activities/execute-cursor/index.ts +419 -602
  90. package/src/activities/execute-cursor/todo-tracker.ts +17 -59
  91. package/src/activities/execute-cursor/turn-boundary.ts +274 -0
  92. package/src/activities/execute-cursor/turn-stream.ts +418 -0
  93. package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +252 -0
  94. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +11 -0
  95. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +78 -0
  96. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +105 -1
  97. package/src/activities/execute-deep-agent/command-provenance.ts +102 -0
  98. package/src/activities/execute-deep-agent/index.ts +91 -4
  99. package/src/activities/execute-deep-agent/status-builder.ts +9 -0
  100. package/src/activities/execute-deep-agent/streaming-v3.ts +6 -1
  101. package/src/activities/execute-deep-agent/streaming.ts +12 -1
  102. package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
  103. package/src/otel.ts +8 -0
  104. package/src/shared/__tests__/implement-plan-prompt.test.ts +40 -2
  105. package/src/shared/__tests__/plan-artifact.test.ts +142 -9
  106. package/src/shared/__tests__/todos.test.ts +216 -0
  107. package/src/shared/filereview/__tests__/capture.test.ts +182 -1
  108. package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
  109. package/src/shared/filereview/__tests__/cas-substrate.test.ts +100 -0
  110. package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
  111. package/src/shared/filereview/__tests__/events.test.ts +105 -0
  112. package/src/shared/filereview/__tests__/line-counts.test.ts +73 -0
  113. package/src/shared/filereview/__tests__/progress.test.ts +338 -0
  114. package/src/shared/filereview/capture.ts +134 -30
  115. package/src/shared/filereview/cas-progress.ts +170 -0
  116. package/src/shared/filereview/cas-substrate.ts +79 -18
  117. package/src/shared/filereview/command-provenance.ts +180 -0
  118. package/src/shared/filereview/events.ts +50 -0
  119. package/src/shared/filereview/git-substrate.ts +114 -0
  120. package/src/shared/filereview/index.ts +36 -1
  121. package/src/shared/filereview/line-counts.ts +83 -0
  122. package/src/shared/filereview/progress.ts +310 -0
  123. package/src/shared/implement-plan-prompt.ts +39 -8
  124. package/src/shared/plan-artifact.ts +152 -16
  125. package/src/shared/plan-mode-prompt.ts +28 -5
  126. package/src/shared/todos.ts +126 -0
  127. package/src/shared/tool-row.ts +34 -0
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Plan mode's output contract spans three components that must agree:
5
5
  * - the model produces the plan as its FINAL message (this directive),
6
- * - the runner publishes that final message verbatim as a `plan.md` artifact
7
- * (`plan-artifact.ts` — `extractFinalPlanText`),
6
+ * - the runner publishes that final message verbatim as a plan markdown
7
+ * artifact, named from the plan's title (`plan-artifact.ts` —
8
+ * `extractFinalPlanText`, `planArtifactName`),
8
9
  * - the SDK promotes the same message to a first-class plan document in the
9
10
  * thread and offers "Build from plan".
10
11
  *
@@ -24,7 +25,17 @@
24
25
  /**
25
26
  * Directive body injected into the system prompt of every Plan-mode
26
27
  * execution. Deliberately explicit about the deliverable's shape: the final
27
- * message is published verbatim as `plan.md`, so a fenced or chat-suffixed
28
- * plan degrades the reviewable document the user sees.
28
+ * message is published verbatim as a plan document (whose filename is derived
29
+ * from the leading `#` title), so a fenced or chat-suffixed plan degrades the
30
+ * reviewable document the user sees.
31
+ *
32
+ * Fence hygiene inside the document is part of the same contract. A plan
33
+ * frequently quotes file content that itself contains fenced code blocks
34
+ * ("insert this section into README.md"); with a same-length outer fence the
35
+ * inner block's closer terminates the outer fence early and corrupts the
36
+ * rendered document — on every client (the react SDK and the terminal
37
+ * renderer alike). And because the plan viewers render top-level ```mermaid
38
+ * fences as diagrams, the directive steers diagrams into the plan body proper
39
+ * rather than leaving them buried, unrendered, inside quoted file content.
29
40
  */
30
41
  export declare const PLAN_MODE_DIRECTIVE: string;
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Plan mode's output contract spans three components that must agree:
5
5
  * - the model produces the plan as its FINAL message (this directive),
6
- * - the runner publishes that final message verbatim as a `plan.md` artifact
7
- * (`plan-artifact.ts` — `extractFinalPlanText`),
6
+ * - the runner publishes that final message verbatim as a plan markdown
7
+ * artifact, named from the plan's title (`plan-artifact.ts` —
8
+ * `extractFinalPlanText`, `planArtifactName`),
8
9
  * - the SDK promotes the same message to a first-class plan document in the
9
10
  * thread and offers "Build from plan".
10
11
  *
@@ -24,8 +25,18 @@
24
25
  /**
25
26
  * Directive body injected into the system prompt of every Plan-mode
26
27
  * execution. Deliberately explicit about the deliverable's shape: the final
27
- * message is published verbatim as `plan.md`, so a fenced or chat-suffixed
28
- * plan degrades the reviewable document the user sees.
28
+ * message is published verbatim as a plan document (whose filename is derived
29
+ * from the leading `#` title), so a fenced or chat-suffixed plan degrades the
30
+ * reviewable document the user sees.
31
+ *
32
+ * Fence hygiene inside the document is part of the same contract. A plan
33
+ * frequently quotes file content that itself contains fenced code blocks
34
+ * ("insert this section into README.md"); with a same-length outer fence the
35
+ * inner block's closer terminates the outer fence early and corrupts the
36
+ * rendered document — on every client (the react SDK and the terminal
37
+ * renderer alike). And because the plan viewers render top-level ```mermaid
38
+ * fences as diagrams, the directive steers diagrams into the plan body proper
39
+ * rather than leaving them buried, unrendered, inside quoted file content.
29
40
  */
30
41
  export const PLAN_MODE_DIRECTIVE = [
31
42
  "IMPORTANT: You are in Plan mode — a read-only analysis turn whose " +
@@ -37,13 +48,25 @@ export const PLAN_MODE_DIRECTIVE = [
37
48
  "- Only read, search, and analyze.",
38
49
  "",
39
50
  "Deliverable — your FINAL message IS the plan. It is published verbatim " +
40
- "as a `plan.md` document that the user reviews and builds from, so:",
51
+ "as a plan document that the user reviews and builds from, so:",
41
52
  "- Write it as a complete, well-structured markdown document: start with " +
42
53
  "a single `#` title and organize the work under `##` section headings. " +
43
54
  "Use lists and tables where they aid scanning.",
55
+ '- Give the `#` title a concise, descriptive name for the work itself; do ' +
56
+ 'NOT prefix it with "Plan:" (this document is already a plan — the ' +
57
+ "prefix is redundant and leaks into the plan's filename).",
44
58
  "- Reference concrete file paths and describe the specific changes " +
45
59
  "planned for each.",
46
60
  "- Do NOT wrap the document in a code fence.",
61
+ "- When quoting content that itself contains fenced code blocks (e.g. a " +
62
+ "proposed file section with a code sample inside), open the outer fence " +
63
+ "with MORE backticks than any inner fence (four or more) — a same-length " +
64
+ "inner closer would terminate the outer fence early and corrupt the " +
65
+ "rendered document.",
66
+ "- Fenced ```mermaid blocks at the top level of the document render as " +
67
+ "diagrams in the plan viewer. When a diagram helps communicate the " +
68
+ "design (architecture, flows), include it directly in the plan body — " +
69
+ "not only inside quoted file content, where it stays unrendered source.",
47
70
  '- Do NOT end with conversational closers ("Let me know...", "Shall I ' +
48
71
  'proceed?") — the next step is the user\'s Build action, and trailing ' +
49
72
  "chat would be published as part of the document.",
@@ -1 +1 @@
1
- {"version":3,"file":"plan-mode-prompt.js","sourceRoot":"","sources":["../../src/shared/plan-mode-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,oEAAoE;QAClE,wCAAwC;IAC1C,EAAE;IACF,cAAc;IACd,6CAA6C;IAC7C,yEAAyE;IACzE,mCAAmC;IACnC,EAAE;IACF,yEAAyE;QACvE,oEAAoE;IACtE,0EAA0E;QACxE,wEAAwE;QACxE,+CAA+C;IACjD,oEAAoE;QAClE,mBAAmB;IACrB,6CAA6C;IAC7C,uEAAuE;QACrE,uEAAuE;QACvE,kDAAkD;CACrD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC"}
1
+ {"version":3,"file":"plan-mode-prompt.js","sourceRoot":"","sources":["../../src/shared/plan-mode-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,oEAAoE;QAClE,wCAAwC;IAC1C,EAAE;IACF,cAAc;IACd,6CAA6C;IAC7C,yEAAyE;IACzE,mCAAmC;IACnC,EAAE;IACF,yEAAyE;QACvE,+DAA+D;IACjE,0EAA0E;QACxE,wEAAwE;QACxE,+CAA+C;IACjD,2EAA2E;QACzE,oEAAoE;QACpE,0DAA0D;IAC5D,oEAAoE;QAClE,mBAAmB;IACrB,6CAA6C;IAC7C,yEAAyE;QACvE,yEAAyE;QACzE,0EAA0E;QAC1E,qEAAqE;QACrE,oBAAoB;IACtB,wEAAwE;QACtE,oEAAoE;QACpE,uEAAuE;QACvE,wEAAwE;IAC1E,uEAAuE;QACrE,uEAAuE;QACvE,kDAAkD;CACrD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Todo-list projection shared by every harness's status writer.
3
+ *
4
+ * All harnesses expose an agent to-do tool — the Cursor SDK emits `TodoWrite`
5
+ * (legacy) / `updateTodos` (current), the native deepagents harness emits
6
+ * `write_todos` — and each writes the SAME `AgentExecutionStatus.todos` proto
7
+ * map that the clients (React `TodoCard`, CLI) render. This module is the single
8
+ * place that maps a raw tool payload into that map, so the Cursor `TodoTracker`
9
+ * and the native v2/v3 status builders cannot drift in how they build todos.
10
+ *
11
+ * The mapping is a superset of what any one harness emits: the Cursor SDK sends
12
+ * per-item `id`/`created_at` and can send a `merge` flag and camelCase statuses;
13
+ * deepagents sends only `{ content, status }` (three statuses, no id, no merge).
14
+ * One helper handling the superset serves all three writers.
15
+ */
16
+ import type { TodoItem } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/todo_pb";
17
+ /**
18
+ * A single raw todo as it arrives in a to-do tool call's arguments. Every field
19
+ * is optional and defensively typed: the Cursor `updateTodos` schema omits
20
+ * per-item `id`, deepagents omits `id`/`created_at`, and the values reach us as
21
+ * untyped JSON (a Cursor `args` string or a protobuf `JsonObject` value).
22
+ */
23
+ export interface RawTodoItem {
24
+ id?: string;
25
+ content?: string;
26
+ status?: string;
27
+ created_at?: string;
28
+ }
29
+ /**
30
+ * Project a to-do tool call's payload into a `status.todos` map, in place.
31
+ * Returns whether the map changed — the caller's "dirty" / force-persist signal.
32
+ *
33
+ * Semantics (identical across all three harness writers):
34
+ * - Empty or non-array `rawTodos`: a full-replace (`!merge`) clears the map and
35
+ * is a change; a merge is a no-op (nothing to merge in) and is not.
36
+ * - Otherwise a full-replace clears first, then every item is (re)written.
37
+ * - `id` is the provided id or a stable index fallback (`todo-<i>`); order is
38
+ * the array order, so index ids are stable across full-replace updates.
39
+ * - `created_at` is preserved from the prior same-id entry ONLY on merge; a
40
+ * full-replace clears first, so it becomes `raw.created_at || now`. (The
41
+ * native harness is always full-replace and sends no `created_at`, so a
42
+ * native todo's `created_at` is `now` on each write — deliberately identical
43
+ * to the Cursor non-merge path.)
44
+ * - `updated_at` is always `now`.
45
+ *
46
+ * Note on clearing: an empty full-replace clears the in-memory map, but the Go
47
+ * `update_status` activity replaces persisted todos only when the incoming map
48
+ * is non-empty, so a settled list survives in history — the clear never reaches
49
+ * the record. This is intentional and relied upon by both harnesses.
50
+ */
51
+ export declare function applyTodoUpdate(target: {
52
+ [key: string]: TodoItem;
53
+ }, rawTodos: unknown, opts: {
54
+ merge: boolean;
55
+ now?: string;
56
+ }): boolean;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Todo-list projection shared by every harness's status writer.
3
+ *
4
+ * All harnesses expose an agent to-do tool — the Cursor SDK emits `TodoWrite`
5
+ * (legacy) / `updateTodos` (current), the native deepagents harness emits
6
+ * `write_todos` — and each writes the SAME `AgentExecutionStatus.todos` proto
7
+ * map that the clients (React `TodoCard`, CLI) render. This module is the single
8
+ * place that maps a raw tool payload into that map, so the Cursor `TodoTracker`
9
+ * and the native v2/v3 status builders cannot drift in how they build todos.
10
+ *
11
+ * The mapping is a superset of what any one harness emits: the Cursor SDK sends
12
+ * per-item `id`/`created_at` and can send a `merge` flag and camelCase statuses;
13
+ * deepagents sends only `{ content, status }` (three statuses, no id, no merge).
14
+ * One helper handling the superset serves all three writers.
15
+ */
16
+ import { create } from "@bufbuild/protobuf";
17
+ import { TodoItemSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/todo_pb";
18
+ import { TodoStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
19
+ import { utcTimestamp } from "./status.js";
20
+ /**
21
+ * Raw status string -> proto `TodoStatus`. Keyed on the lowercased status so
22
+ * the current SDK's camelCase `inProgress` and the legacy snake_case
23
+ * `in_progress` both resolve. `cancelled` is only ever emitted by the Cursor
24
+ * SDK — deepagents' three-status schema never sends it — but it is kept here so
25
+ * the one shared mapping is faithful to every producer. Unknown values fall back
26
+ * to `TODO_PENDING` at the call site.
27
+ */
28
+ const STATUS_MAP = {
29
+ pending: TodoStatus.TODO_PENDING,
30
+ in_progress: TodoStatus.TODO_IN_PROGRESS,
31
+ inprogress: TodoStatus.TODO_IN_PROGRESS,
32
+ completed: TodoStatus.TODO_COMPLETED,
33
+ cancelled: TodoStatus.TODO_CANCELLED,
34
+ };
35
+ /**
36
+ * Project a to-do tool call's payload into a `status.todos` map, in place.
37
+ * Returns whether the map changed — the caller's "dirty" / force-persist signal.
38
+ *
39
+ * Semantics (identical across all three harness writers):
40
+ * - Empty or non-array `rawTodos`: a full-replace (`!merge`) clears the map and
41
+ * is a change; a merge is a no-op (nothing to merge in) and is not.
42
+ * - Otherwise a full-replace clears first, then every item is (re)written.
43
+ * - `id` is the provided id or a stable index fallback (`todo-<i>`); order is
44
+ * the array order, so index ids are stable across full-replace updates.
45
+ * - `created_at` is preserved from the prior same-id entry ONLY on merge; a
46
+ * full-replace clears first, so it becomes `raw.created_at || now`. (The
47
+ * native harness is always full-replace and sends no `created_at`, so a
48
+ * native todo's `created_at` is `now` on each write — deliberately identical
49
+ * to the Cursor non-merge path.)
50
+ * - `updated_at` is always `now`.
51
+ *
52
+ * Note on clearing: an empty full-replace clears the in-memory map, but the Go
53
+ * `update_status` activity replaces persisted todos only when the incoming map
54
+ * is non-empty, so a settled list survives in history — the clear never reaches
55
+ * the record. This is intentional and relied upon by both harnesses.
56
+ */
57
+ export function applyTodoUpdate(target, rawTodos, opts) {
58
+ const { merge } = opts;
59
+ const now = opts.now ?? utcTimestamp();
60
+ if (!Array.isArray(rawTodos) || rawTodos.length === 0) {
61
+ if (!merge) {
62
+ clearMap(target);
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+ if (!merge) {
68
+ clearMap(target);
69
+ }
70
+ for (let i = 0; i < rawTodos.length; i++) {
71
+ const raw = coerceRawTodo(rawTodos[i]);
72
+ const id = raw.id || `todo-${i}`;
73
+ const statusStr = (raw.status ?? "pending").toLowerCase();
74
+ const status = STATUS_MAP[statusStr] ?? TodoStatus.TODO_PENDING;
75
+ // Preserve the original creation time only when merging into an existing
76
+ // entry; a full-replace cleared the map above, so `existing` is undefined.
77
+ const existing = merge ? target[id] : undefined;
78
+ target[id] = create(TodoItemSchema, {
79
+ id,
80
+ content: raw.content ?? "",
81
+ status,
82
+ createdAt: existing?.createdAt || raw.created_at || now,
83
+ updatedAt: now,
84
+ });
85
+ }
86
+ return true;
87
+ }
88
+ /** Narrow an untyped array element to a {@link RawTodoItem}; non-objects become empty. */
89
+ function coerceRawTodo(value) {
90
+ return typeof value === "object" && value !== null ? value : {};
91
+ }
92
+ /** Delete every key from the map in place (proto map fields have no `.clear()`). */
93
+ function clearMap(target) {
94
+ for (const key of Object.keys(target)) {
95
+ delete target[key];
96
+ }
97
+ }
98
+ //# sourceMappingURL=todos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todos.js","sourceRoot":"","sources":["../../src/shared/todos.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,8DAA8D,CAAC;AAE9F,OAAO,EAAE,UAAU,EAAE,MAAM,8DAA8D,CAAC;AAC1F,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAe3C;;;;;;;GAOG;AACH,MAAM,UAAU,GAA+B;IAC7C,OAAO,EAAE,UAAU,CAAC,YAAY;IAChC,WAAW,EAAE,UAAU,CAAC,gBAAgB;IACxC,UAAU,EAAE,UAAU,CAAC,gBAAgB;IACvC,SAAS,EAAE,UAAU,CAAC,cAAc;IACpC,SAAS,EAAE,UAAU,CAAC,cAAc;CACrC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAmC,EACnC,QAAiB,EACjB,IAAsC;IAEtC,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACvB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,YAAY,EAAE,CAAC;IAEvC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,QAAQ,CAAC,MAAM,CAAC,CAAC;YACjB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnB,CAAC;IAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,IAAI,QAAQ,CAAC,EAAE,CAAC;QACjC,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC;QAC1D,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC,YAAY,CAAC;QAEhE,yEAAyE;QACzE,2EAA2E;QAC3E,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAEhD,MAAM,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,cAAc,EAAE;YAClC,EAAE;YACF,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE;YAC1B,MAAM;YACN,SAAS,EAAE,QAAQ,EAAE,SAAS,IAAI,GAAG,CAAC,UAAU,IAAI,GAAG;YACvD,SAAS,EAAE,GAAG;SACf,CAAC,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0FAA0F;AAC1F,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAE,KAAqB,CAAC,CAAC,CAAC,EAAE,CAAC;AACnF,CAAC;AAED,oFAAoF;AACpF,SAAS,QAAQ,CAAC,MAAmC;IACnD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;AACH,CAAC"}
@@ -115,3 +115,19 @@ export declare function isToolCallRowHidden(tc: ToolCall): boolean;
115
115
  * none; Cursor clones them in) — the snapshot reflects whatever is present.
116
116
  */
117
117
  export declare function collectSubAgentToolCallIds(subAgents: readonly SubAgentExecution[]): Set<string>;
118
+ /**
119
+ * Collect the ids of tool-call rows that have already SETTLED (reached a terminal
120
+ * state: completed, failed, or skipped) in a transcript.
121
+ *
122
+ * The deep-agent turn-boundary provenance derivation (DD-28) snapshots this
123
+ * BEFORE a turn's stream to scope "this turn's tool calls" by identity: a call
124
+ * whose id is absent from the snapshot is this-turn's — either freshly streamed,
125
+ * or a prior gate that was WAITING_APPROVAL before the stream and executes now on
126
+ * resume (StatusBuilder updates the seeded row in place, so it keeps its id).
127
+ * Unlike the Cursor harness, the deep-agent cannot scope positionally — an
128
+ * approved command executes at its SEEDED position — so identity is the only
129
+ * correct scope, mirroring {@link collectSubAgentToolCallIds}. Terminal (not just
130
+ * completed) so a PRIOR turn's failed/skipped non-shell call is excluded and
131
+ * cannot spuriously disqualify this turn.
132
+ */
133
+ export declare function collectSettledToolCallIds(messages: readonly AgentMessage[]): Set<string>;
@@ -173,4 +173,35 @@ export function collectSubAgentToolCallIds(subAgents) {
173
173
  }
174
174
  return ids;
175
175
  }
176
+ /** Terminal tool-call statuses — a row that has finished (however it finished). */
177
+ const TERMINAL_TOOL_CALL_STATUSES = new Set([
178
+ ToolCallStatus.TOOL_CALL_COMPLETED,
179
+ ToolCallStatus.TOOL_CALL_FAILED,
180
+ ToolCallStatus.TOOL_CALL_SKIPPED,
181
+ ]);
182
+ /**
183
+ * Collect the ids of tool-call rows that have already SETTLED (reached a terminal
184
+ * state: completed, failed, or skipped) in a transcript.
185
+ *
186
+ * The deep-agent turn-boundary provenance derivation (DD-28) snapshots this
187
+ * BEFORE a turn's stream to scope "this turn's tool calls" by identity: a call
188
+ * whose id is absent from the snapshot is this-turn's — either freshly streamed,
189
+ * or a prior gate that was WAITING_APPROVAL before the stream and executes now on
190
+ * resume (StatusBuilder updates the seeded row in place, so it keeps its id).
191
+ * Unlike the Cursor harness, the deep-agent cannot scope positionally — an
192
+ * approved command executes at its SEEDED position — so identity is the only
193
+ * correct scope, mirroring {@link collectSubAgentToolCallIds}. Terminal (not just
194
+ * completed) so a PRIOR turn's failed/skipped non-shell call is excluded and
195
+ * cannot spuriously disqualify this turn.
196
+ */
197
+ export function collectSettledToolCallIds(messages) {
198
+ const ids = new Set();
199
+ for (const msg of messages) {
200
+ for (const tc of msg.toolCalls) {
201
+ if (TERMINAL_TOOL_CALL_STATUSES.has(tc.status))
202
+ ids.add(tc.id);
203
+ }
204
+ }
205
+ return ids;
206
+ }
176
207
  //# sourceMappingURL=tool-row.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-row.js","sourceRoot":"","sources":["../../src/shared/tool-row.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-row.js","sourceRoot":"","sources":["../../src/shared/tool-row.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/runner",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "description": "Embeddable Temporal worker for the Stigmer AI agent platform — handles agent execution, workflow orchestration, and MCP server management",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -85,7 +85,7 @@
85
85
  "@opentelemetry/sdk-metrics": "^2.0.0",
86
86
  "@opentelemetry/sdk-trace-base": "^2.0.0",
87
87
  "@opentelemetry/sdk-trace-node": "^2.0.0",
88
- "@stigmer/protos": "3.1.2",
88
+ "@stigmer/protos": "3.1.4",
89
89
  "@temporalio/activity": "^1.11.0",
90
90
  "@temporalio/client": "^1.11.0",
91
91
  "@temporalio/common": "^1.11.0",
@@ -93,6 +93,7 @@
93
93
  "@temporalio/worker": "^1.11.0",
94
94
  "@temporalio/workflow": "^1.16.2",
95
95
  "deepagents": "^1.10.0",
96
+ "diff": "^8.0.4",
96
97
  "jq-wasm": "^1.1.0-jq-1.8.1",
97
98
  "js-yaml": "^4.1.1",
98
99
  "proper-lockfile": "^4.1.2",
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // Spy span/tracer, hoisted so the vi.mock factory (which runs before imports)
4
+ // can close over them without a temporal-dead-zone error.
5
+ const { spanEnd, spanSetAttribute, startSpan } = vi.hoisted(() => {
6
+ const spanEnd = vi.fn();
7
+ const spanSetAttribute = vi.fn();
8
+ const startSpan = vi.fn(() => ({ end: spanEnd, setAttribute: spanSetAttribute }));
9
+ return { spanEnd, spanSetAttribute, startSpan };
10
+ });
11
+
12
+ vi.mock("@opentelemetry/api", () => ({
13
+ trace: { getTracer: () => ({ startSpan }) },
14
+ }));
15
+
16
+ import {
17
+ startCursorTurnSpan,
18
+ ATTR_LLM_INPUT_TOKENS,
19
+ ATTR_LLM_OUTPUT_TOKENS,
20
+ } from "../otel.js";
21
+
22
+ const spanAttrs = { model: "gpt-x", mode: "local", sessionId: "sess-1" };
23
+
24
+ describe("startCursorTurnSpan", () => {
25
+ beforeEach(() => {
26
+ spanEnd.mockClear();
27
+ spanSetAttribute.mockClear();
28
+ startSpan.mockClear();
29
+ });
30
+
31
+ it("records the input and output token attributes", async () => {
32
+ const span = await startCursorTurnSpan(spanAttrs);
33
+ span.setTokens(1200, 340);
34
+ expect(spanSetAttribute).toHaveBeenCalledWith(ATTR_LLM_INPUT_TOKENS, 1200);
35
+ expect(spanSetAttribute).toHaveBeenCalledWith(ATTR_LLM_OUTPUT_TOKENS, 340);
36
+ });
37
+
38
+ it("omits zero-valued token attributes (no count = no attribute)", async () => {
39
+ const span = await startCursorTurnSpan(spanAttrs);
40
+ span.setTokens(0, 0);
41
+ expect(spanSetAttribute).not.toHaveBeenCalled();
42
+ });
43
+
44
+ it("ends the span exactly once even when end() is called repeatedly", async () => {
45
+ // finishTurnTelemetry is invoked from the activity's finally on every exit
46
+ // path; a second end() (e.g. a defensive double-invoke) must be a no-op so
47
+ // OTel never warns about ending an already-ended span.
48
+ const span = await startCursorTurnSpan(spanAttrs);
49
+ span.end();
50
+ span.end();
51
+ span.end();
52
+ expect(spanEnd).toHaveBeenCalledTimes(1);
53
+ });
54
+
55
+ it("ignores setTokens after the span has ended (no late mutation)", async () => {
56
+ const span = await startCursorTurnSpan(spanAttrs);
57
+ span.end();
58
+ span.setTokens(1200, 340);
59
+ expect(spanSetAttribute).not.toHaveBeenCalled();
60
+ });
61
+ });
@@ -332,6 +332,13 @@ describe("formatImplementPlanSection", () => {
332
332
  expect(formatImplementPlanSection(undefined, [PLAN_PATH])).toBeUndefined();
333
333
  });
334
334
 
335
+ it("carries the plan-derived progress-tracking instruction (Tier 3)", () => {
336
+ const section = formatImplementPlanSection(true, [PLAN_PATH]);
337
+
338
+ expect(section).toContain("to-do list");
339
+ expect(section).toContain("break the plan into");
340
+ });
341
+
335
342
  it("injects the directive into a build-from-plan first prompt", () => {
336
343
  const prompt = buildPrompt(
337
344
  input({
@@ -240,3 +240,71 @@ describe("DeltaEnricher finalize reconciliation", () => {
240
240
  expect(messages[1].toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
241
241
  });
242
242
  });
243
+
244
+ describe("DeltaEnricher recovery-retry stream epilogue", () => {
245
+ // Regression for the zombie RUNNING row of aex_01kws27q1e2esvkqjpvectttxf:
246
+ // the recovery retry's bare stream loop buffered the tool-call-completed
247
+ // delta but never flushed it, so the row lacked the completedAt evidence
248
+ // finalize's sweep requires and persisted as RUNNING forever. The epilogue
249
+ // must applyEnrichments (flush the buffered evidence) BEFORE finalize.
250
+ it("applyEnrichments + finalize promotes a retry's buffered-completion RUNNING row", () => {
251
+ vi.spyOn(console, "log").mockImplementation(() => {});
252
+
253
+ const enricher = new DeltaEnricher();
254
+ const messages: AgentMessage[] = [
255
+ makeMessage([{ id: "tc-retry", name: "Shell", status: ToolCallStatus.TOOL_CALL_RUNNING }]),
256
+ ];
257
+
258
+ // The retry's onDelta buffered the completion, but the bare retry loop
259
+ // never applied it to the transcript.
260
+ enricher.processDelta({
261
+ type: "tool-call-completed",
262
+ callId: "tc-retry",
263
+ toolCall: { type: "shell" },
264
+ } as unknown as InteractionUpdate);
265
+
266
+ // Without the flush, finalize alone cannot promote (no evidence on the row).
267
+ const unflushed = makeMessage([
268
+ { id: "tc-retry", name: "Shell", status: ToolCallStatus.TOOL_CALL_RUNNING },
269
+ ]);
270
+ new DeltaEnricher().finalize([unflushed]);
271
+ expect(unflushed.toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
272
+
273
+ // The epilogue sequence: flush the buffered evidence, then finalize.
274
+ enricher.applyEnrichments(messages);
275
+ enricher.finalize(messages);
276
+
277
+ const tc = messages[0].toolCalls[0];
278
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
279
+ expect(tc.completedAt).toBeTruthy();
280
+ });
281
+
282
+ it("re-running applyEnrichments and finalize is idempotent", () => {
283
+ vi.spyOn(console, "log").mockImplementation(() => {});
284
+
285
+ const enricher = new DeltaEnricher();
286
+ const messages: AgentMessage[] = [
287
+ makeMessage([{ id: "tc-1", name: "Shell", status: ToolCallStatus.TOOL_CALL_RUNNING }]),
288
+ ];
289
+
290
+ enricher.processDelta({
291
+ type: "tool-call-completed",
292
+ callId: "tc-1",
293
+ toolCall: { type: "shell" },
294
+ } as unknown as InteractionUpdate);
295
+
296
+ // Primary epilogue ...
297
+ enricher.applyEnrichments(messages);
298
+ enricher.finalize(messages);
299
+ const stamped = messages[0].toolCalls[0].completedAt;
300
+
301
+ // ... and the retry epilogue re-runs both. The row must keep its original
302
+ // completion evidence and terminal status.
303
+ enricher.applyEnrichments(messages);
304
+ enricher.finalize(messages);
305
+
306
+ const tc = messages[0].toolCalls[0];
307
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
308
+ expect(tc.completedAt).toBe(stamped);
309
+ });
310
+ });