@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
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Mid-run live capture — the harness-agnostic glue that turns a per-turn
3
+ * workspace delta into the transient `AgentExecutionStatus.file_change_progress`
4
+ * snapshot the "N files changed so far" strip renders (DD-32 / DD-33).
5
+ *
6
+ * THE MODEL
7
+ * ---------
8
+ * This is category 2 of the two status patterns: a runner-owned, latest-snapshot
9
+ * DISPLAY field, NOT the event-sourced file-review ledger. It is deliberately NOT
10
+ * a `FileChangeSet` — progress is never in the ledger, carries NO file bytes or
11
+ * digests, and is NEVER decidable. The turn-boundary CANDIDATE_CAPTURED (in
12
+ * `file_change_sets`) remains the single authoritative, reviewable diff; a mid-run
13
+ * snapshot is no more authoritative than a streamed tool-call arg.
14
+ *
15
+ * THE SUBSTRATE ABSTRACTION (DD-33)
16
+ * ---------------------------------
17
+ * Where the delta comes from differs by workspace: a git tree diffs cheaply with
18
+ * `git --numstat` (the git substrate), a non-git / gitignored workspace reads the
19
+ * CAS observer (the cas substrate, {@link ./cas-progress.js}), and a git tree
20
+ * with gitignored writes composes both (the hybrid substrate). A
21
+ * {@link ProgressSubstrate} hides that behind one `capture()` so the floor + attach
22
+ * logic below is written once. Each substrate is a per-turn object owning its own
23
+ * short-circuit cache; `capture()` always returns the FULL cumulative turn delta
24
+ * plus a `changed` flag, so the hybrid can merge both slices even when only one
25
+ * moved (a `ProgressDelta | undefined` would drop the unchanged slice).
26
+ *
27
+ * SECRET SAFETY
28
+ * -------------
29
+ * No file bodies are ever carried. A secret-like path ({@link isSecretLikePath})
30
+ * is still surfaced (path visible) but with its line counts ZEROED — the same
31
+ * "path visible, content withheld" rule the ledger uses (DD-12). The cas
32
+ * substrate additionally excludes gate-blocked secrets up front
33
+ * ({@link partitionIgnoredPathsBySecret}); this zeroing is the belt-and-suspenders
34
+ * backstop. Nothing new can leak through this field.
35
+ *
36
+ * @since File-Change HITL Redesign (mid-run live capture / DD-32; non-git + hybrid / DD-33)
37
+ */
38
+ import { create } from "@bufbuild/protobuf";
39
+ import { FileChangeProgressEntrySchema, FileChangeProgressSchema, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
40
+ import { utcTimestamp } from "../status.js";
41
+ import { toFileChangeKind } from "./capture.js";
42
+ import { captureProgressDelta, } from "./git-substrate.js";
43
+ import { isSecretLikePath } from "./secret-paths.js";
44
+ /**
45
+ * Cap the per-file entry list so a turn touching thousands of files does not
46
+ * bloat the persisted status (which rides Temporal / the store). `files_changed`
47
+ * and the aggregate counts stay honest over ALL files; the strip shows "… and K
48
+ * more" when the list is capped. Also the cas substrate's read budget — it reads
49
+ * after-bytes for at most this many files per capture (DD-33).
50
+ */
51
+ export const PROGRESS_MAX_ENTRIES = 200;
52
+ /**
53
+ * Minimum wall-clock gap between two mid-run captures, in ms. A floor on cost:
54
+ * every capture stages the working tree (git) or reads the touched set (cas), so
55
+ * we bound how often that runs regardless of persist frequency. Env-tunable for
56
+ * large-repo deployments; defaults to 2s (feels live without hammering the disk).
57
+ */
58
+ export const PROGRESS_CAPTURE_MIN_INTERVAL_MS = readMinIntervalMs();
59
+ function readMinIntervalMs() {
60
+ const raw = process.env.STIGMER_PROGRESS_CAPTURE_MIN_INTERVAL_MS;
61
+ if (!raw)
62
+ return 2000;
63
+ const n = Number.parseInt(raw, 10);
64
+ return Number.isFinite(n) && n >= 0 ? n : 2000;
65
+ }
66
+ /**
67
+ * Whether enough wall-clock has elapsed since the last capture to take another.
68
+ * Pure and separated for direct testing (mirrors `persist-decision.ts`). A
69
+ * `lastAtMs` of 0 (never captured) always passes.
70
+ */
71
+ export function shouldCaptureProgress(lastAtMs, nowMs, minIntervalMs = PROGRESS_CAPTURE_MIN_INTERVAL_MS) {
72
+ return nowMs - lastAtMs >= minIntervalMs;
73
+ }
74
+ /**
75
+ * Build the transient {@link FileChangeProgress} message from a substrate-neutral
76
+ * delta. Zeroes counts for secret-like paths (path visible, magnitude withheld),
77
+ * caps the entry list at {@link PROGRESS_MAX_ENTRIES}, and reports the honest
78
+ * `files_changed` (`delta.totalFilesChanged` when a substrate capped its reads,
79
+ * else the entry count). Aggregate counts sum over the emitted entries. Pure and
80
+ * exported for direct testing.
81
+ */
82
+ export function buildFileChangeProgress(delta, changeSetId) {
83
+ let totalAdded = 0;
84
+ let totalRemoved = 0;
85
+ const entries = [];
86
+ for (const entry of delta.entries) {
87
+ // A secret-like path is surfaced (path visible) but its magnitude is withheld
88
+ // — counts zeroed, mirroring the ledger's "path visible, content withheld".
89
+ const secret = isSecretLikePath(entry.pathAfter || entry.pathBefore);
90
+ const linesAdded = secret ? 0 : entry.linesAdded;
91
+ const linesRemoved = secret ? 0 : entry.linesRemoved;
92
+ totalAdded += linesAdded;
93
+ totalRemoved += linesRemoved;
94
+ if (entries.length < PROGRESS_MAX_ENTRIES) {
95
+ entries.push(create(FileChangeProgressEntrySchema, {
96
+ pathBefore: entry.pathBefore,
97
+ pathAfter: entry.pathAfter,
98
+ kind: entry.kind,
99
+ linesAdded,
100
+ linesRemoved,
101
+ }));
102
+ }
103
+ }
104
+ return create(FileChangeProgressSchema, {
105
+ changeSetId,
106
+ filesChanged: delta.totalFilesChanged ?? delta.entries.length,
107
+ linesAdded: totalAdded,
108
+ linesRemoved: totalRemoved,
109
+ entries,
110
+ capturedAt: utcTimestamp(),
111
+ });
112
+ }
113
+ /** A fresh per-turn progress state. */
114
+ export function newProgressCaptureState() {
115
+ return { lastAtMs: 0 };
116
+ }
117
+ /**
118
+ * Capture-and-attach the live progress snapshot onto `status.file_change_progress`,
119
+ * throttled by the floor. Mutates `status` and `state` in place; a no-op when the
120
+ * floor has not elapsed or the substrate reports nothing changed. Content-free and
121
+ * secret-safe (see {@link buildFileChangeProgress}). Called by both harnesses from
122
+ * their per-persist loop, each passing a substrate built for its workspace shape.
123
+ *
124
+ * The field is set even when the delta is now EMPTY (the agent reverted its own
125
+ * edits) so the strip reflects the reversion (it hides at zero) rather than
126
+ * showing a stale count — the server's presence-guarded merge would keep the
127
+ * stale value if the runner omitted the field.
128
+ */
129
+ export async function captureFileChangeProgress(opts) {
130
+ const now = opts.nowMs ?? Date.now();
131
+ if (!shouldCaptureProgress(opts.state.lastAtMs, now))
132
+ return;
133
+ opts.state.lastAtMs = now;
134
+ const { delta, changed } = await opts.substrate.capture();
135
+ if (!changed)
136
+ return;
137
+ opts.status.fileChangeProgress = buildFileChangeProgress(delta, opts.changeSetId);
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Substrates
141
+ // ---------------------------------------------------------------------------
142
+ function gitEntryToProgressEntry(e) {
143
+ return {
144
+ pathBefore: e.pathBefore,
145
+ pathAfter: e.pathAfter,
146
+ kind: toFileChangeKind(e.changeType),
147
+ linesAdded: e.linesAdded,
148
+ linesRemoved: e.linesRemoved,
149
+ };
150
+ }
151
+ /**
152
+ * The git substrate: the working-tree `--numstat` delta against the pinned
153
+ * baseline. `git add -A` honors `.gitignore`, so this covers exactly the
154
+ * git-TRACKED changes (from any source — tool or shell), disjoint from the cas
155
+ * substrate's gitignored set. Preserves the tree-sha short-circuit: on an
156
+ * unchanged tree it returns the cached full delta with `changed:false` (the
157
+ * caller skips the re-attach), byte-identical to the pre-DD-33 behavior.
158
+ */
159
+ export function createGitProgressSubstrate(opts) {
160
+ let lastTreeSha;
161
+ let cachedFull = { entries: [] };
162
+ return {
163
+ async capture() {
164
+ const gitDelta = await captureProgressDelta(opts.workspaceRoot, opts.executionId, opts.baselineTree, opts.excludePaths, lastTreeSha);
165
+ if (gitDelta === undefined) {
166
+ // Working tree unchanged since the last capture — reuse the cache so the
167
+ // hybrid can still merge this (unchanged) slice with a changed cas slice.
168
+ return { delta: cachedFull, changed: false };
169
+ }
170
+ lastTreeSha = gitDelta.afterTree;
171
+ cachedFull = { entries: gitDelta.entries.map(gitEntryToProgressEntry) };
172
+ return { delta: cachedFull, changed: true };
173
+ },
174
+ };
175
+ }
176
+ /**
177
+ * The hybrid substrate: a git tree whose gitignored writes are captured via CAS.
178
+ * Concatenates both slices (disjoint by the `.gitignore` boundary — numstat sees
179
+ * only tracked paths, the observer only gitignored ones) and sums the honest
180
+ * totals. `changed` is true when EITHER slice moved; because each child returns
181
+ * its full cumulative delta, the merged delta always carries both slices.
182
+ */
183
+ export function createHybridProgressSubstrate(git, cas) {
184
+ return {
185
+ async capture() {
186
+ const [g, c] = await Promise.all([git.capture(), cas.capture()]);
187
+ const delta = {
188
+ entries: [...g.delta.entries, ...c.delta.entries],
189
+ totalFilesChanged: (g.delta.totalFilesChanged ?? g.delta.entries.length) +
190
+ (c.delta.totalFilesChanged ?? c.delta.entries.length),
191
+ };
192
+ return { delta, changed: g.changed || c.changed };
193
+ },
194
+ };
195
+ }
196
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.js","sourceRoot":"","sources":["../../../src/shared/filereview/progress.ts"],"names":[],"mappings":"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"}
@@ -11,8 +11,9 @@
11
11
  * The directive has two variants, chosen by whether the approved plan document
12
12
  * actually materialized in the workspace:
13
13
  * - Attached (the normal case): the client uploaded the approved plan text
14
- * (edited or not) and attached it, mounted at `.stigmer/inputs/plan.md`.
15
- * The directive points the model at that file and names it authoritative —
14
+ * (edited or not) and attached it, mounted under `.stigmer/inputs/` as the
15
+ * plan's own file (`<slug>_<id>.plan.md`). The directive points the model at
16
+ * that file and names it authoritative —
16
17
  * the user may have refined the document after the plan turn, so the
17
18
  * conversation's version can be stale.
18
19
  * - Conversation-only (the client's upload failed): the directive falls back
@@ -25,8 +26,9 @@
25
26
  */
26
27
  /**
27
28
  * Find the approved plan document among the workspace paths the harness
28
- * injected for this execution's attachments. Detection keys on the canonical
29
- * plan filename the same convention the UI uses to detect the plan artifact.
29
+ * injected for this execution's attachments. Detection keys on the plan
30
+ * filename convention ({@link isPlanArtifactName} the legacy `plan.md` or any
31
+ * `*.plan.md`), the same convention the UI uses to detect the plan artifact.
30
32
  * Returns `undefined` when no plan attachment landed (upload failed, or the
31
33
  * attachment itself failed to inject), which selects the conversation-only
32
34
  * directive variant.
@@ -11,8 +11,9 @@
11
11
  * The directive has two variants, chosen by whether the approved plan document
12
12
  * actually materialized in the workspace:
13
13
  * - Attached (the normal case): the client uploaded the approved plan text
14
- * (edited or not) and attached it, mounted at `.stigmer/inputs/plan.md`.
15
- * The directive points the model at that file and names it authoritative —
14
+ * (edited or not) and attached it, mounted under `.stigmer/inputs/` as the
15
+ * plan's own file (`<slug>_<id>.plan.md`). The directive points the model at
16
+ * that file and names it authoritative —
16
17
  * the user may have refined the document after the plan turn, so the
17
18
  * conversation's version can be stale.
18
19
  * - Conversation-only (the client's upload failed): the directive falls back
@@ -23,17 +24,44 @@
23
24
  * framing (XML-tag section for Cursor, markdown heading for the native
24
25
  * harness).
25
26
  */
26
- import { PLAN_ARTIFACT_NAME } from "./plan-artifact.js";
27
+ import { isPlanArtifactName } from "./plan-artifact.js";
28
+ /**
29
+ * Progress-tracking instruction shared by both directive variants (Tier 3 of
30
+ * Plan mode — plan-driven build progress). The agent's own to-do tool is the
31
+ * single writer of `status.todos`, so instructing it to derive the list from
32
+ * the approved plan is the entire plan→progress linkage: the runner's todo
33
+ * extraction and the clients' todo renderers light up unchanged.
34
+ *
35
+ * Deliberately tool-agnostic ("your to-do list"): the Cursor harness exposes
36
+ * TodoWrite/updateTodos, the native harness write_todos, and each runtime
37
+ * already teaches the model its own tool.
38
+ *
39
+ * Wording constraint: this block rides BOTH variants, and the conversation-only
40
+ * variant is pinned by tests to never contain "plan.md" (it has no plan file
41
+ * to reference) — so say "the plan", never name the file.
42
+ */
43
+ const TRACK_PROGRESS_INSTRUCTION = [
44
+ "Track your progress with your to-do list so the user can follow the " +
45
+ "build:",
46
+ "- Before you start, break the plan into a concrete, ordered to-do list — " +
47
+ "roughly one item per implementation step.",
48
+ "- As you work, keep it current: mark each item in progress when you " +
49
+ "begin it and completed when it is done.",
50
+ ].join("\n");
27
51
  /**
28
52
  * Find the approved plan document among the workspace paths the harness
29
- * injected for this execution's attachments. Detection keys on the canonical
30
- * plan filename the same convention the UI uses to detect the plan artifact.
53
+ * injected for this execution's attachments. Detection keys on the plan
54
+ * filename convention ({@link isPlanArtifactName} the legacy `plan.md` or any
55
+ * `*.plan.md`), the same convention the UI uses to detect the plan artifact.
31
56
  * Returns `undefined` when no plan attachment landed (upload failed, or the
32
57
  * attachment itself failed to inject), which selects the conversation-only
33
58
  * directive variant.
34
59
  */
35
60
  export function findApprovedPlanPath(attachmentPaths) {
36
- return attachmentPaths.find((p) => p.split("/").pop() === PLAN_ARTIFACT_NAME);
61
+ return attachmentPaths.find((p) => {
62
+ const name = p.split("/").pop();
63
+ return name !== undefined && isPlanArtifactName(name);
64
+ });
37
65
  }
38
66
  /**
39
67
  * Build the implement-plan directive body for a Build-from-plan execution.
@@ -54,6 +82,8 @@ export function buildImplementPlanDirective(planPath) {
54
82
  "That document is the authoritative version of the plan — the user may " +
55
83
  "have edited it after it was proposed, so where it differs from the " +
56
84
  "conversation above, follow the document.",
85
+ "",
86
+ TRACK_PROGRESS_INSTRUCTION,
57
87
  ].join("\n");
58
88
  }
59
89
  return [
@@ -61,6 +91,8 @@ export function buildImplementPlanDirective(planPath) {
61
91
  "APPROVED.",
62
92
  "",
63
93
  "Implement the plan proposed in the conversation above, step by step.",
94
+ "",
95
+ TRACK_PROGRESS_INSTRUCTION,
64
96
  ].join("\n");
65
97
  }
66
98
  //# sourceMappingURL=implement-plan-prompt.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"implement-plan-prompt.js","sourceRoot":"","sources":["../../src/shared/implement-plan-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,eAAkC;IAElC,OAAO,eAAe,CAAC,IAAI,CACzB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,kBAAkB,CACjD,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CAAC,QAAiB;IAC3D,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO;YACL,mEAAmE;gBACjE,WAAW;YACb,EAAE;YACF,+CAA+C,QAAQ,cAAc;gBACnE,wCAAwC;YAC1C,EAAE;YACF,wEAAwE;gBACtE,qEAAqE;gBACrE,0CAA0C;SAC7C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC;IAED,OAAO;QACL,mEAAmE;YACjE,WAAW;QACb,EAAE;QACF,sEAAsE;KACvE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"implement-plan-prompt.js","sourceRoot":"","sources":["../../src/shared/implement-plan-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,MAAM,0BAA0B,GAAG;IACjC,sEAAsE;QACpE,QAAQ;IACV,2EAA2E;QACzE,2CAA2C;IAC7C,sEAAsE;QACpE,yCAAyC;CAC5C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAClC,eAAkC;IAElC,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;QAChC,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;QAChC,OAAO,IAAI,KAAK,SAAS,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CAAC,QAAiB;IAC3D,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO;YACL,mEAAmE;gBACjE,WAAW;YACb,EAAE;YACF,+CAA+C,QAAQ,cAAc;gBACnE,wCAAwC;YAC1C,EAAE;YACF,wEAAwE;gBACtE,qEAAqE;gBACrE,0CAA0C;YAC5C,EAAE;YACF,0BAA0B;SAC3B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC;IAED,OAAO;QACL,mEAAmE;YACjE,WAAW;QACb,EAAE;QACF,sEAAsE;QACtE,EAAE;QACF,0BAA0B;KAC3B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
@@ -2,11 +2,30 @@
2
2
  * Plan-mode artifact publishing.
3
3
  *
4
4
  * When an execution runs in Plan mode (InteractionMode.PLAN), the agent's final
5
- * message IS the plan. We publish that text as a first-class `plan.md`
5
+ * message IS the plan. We publish that text as a first-class plan markdown
6
6
  * ExecutionArtifact so the UI can render a reviewable Plan card with
7
7
  * copy/download, and a follow-up "Implement" execution can reference it
8
8
  * deterministically.
9
9
  *
10
+ * The artifact is named from the plan's own title — a hyphenated slug of its
11
+ * leading `# H1`, a `_<hash>` discriminator, and a `.plan.md` suffix (e.g.
12
+ * `plan-card-ux-cleanup_a1b2c3d4.plan.md`), so a downloaded plan lands as a
13
+ * recognizable file and the card, plan tab, and saved file all agree on one
14
+ * name. A plan with no derivable title falls back to a bare `<hash>.plan.md`.
15
+ *
16
+ * The `_<hash>` discriminator (first 8 hex of the plan content's SHA-256) is
17
+ * NOT for storage uniqueness — storage keys are already execution-scoped
18
+ * (`artifacts/{execId}/<name>`). It exists because the artifact BASENAME is a
19
+ * user-facing shared namespace: downloads save under it (see the artifact
20
+ * download disposition) and the artifact list surfaces it, so two same-titled
21
+ * plans would otherwise collide in the user's Downloads folder and read
22
+ * identically in the list. Deriving it from content (not a random or
23
+ * execution-scoped value) keeps naming honestly idempotent — identical content
24
+ * yields an identical name, so a finalize retry re-uploads to the same key,
25
+ * while any real edit yields a distinct one. This refines DD-23 §D3 ("no
26
+ * uniqueness hash"), which was correct about storage but overlooked the
27
+ * download/list basename namespace introduced by DD-23 §D1.
28
+ *
10
29
  * This is deliberately a single, harness-agnostic helper:
11
30
  * - The native (deepagents) harness already auto-publishes files an agent
12
31
  * writes (InlinePublisher), but Plan mode is read-only, so there is no file to
@@ -21,19 +40,46 @@
21
40
  *
22
41
  * The plan content is NOT duplicated as a separate stored blob beyond this
23
42
  * artifact — the chat message remains the live/streamed view; the artifact is
24
- * the durable/exportable view, detected by convention (a FILE artifact named
25
- * `plan.md`).
43
+ * the durable/exportable view, detected by convention (a FILE artifact whose
44
+ * name satisfies {@link isPlanArtifactName}).
26
45
  */
27
46
  import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
28
47
  import type { ArtifactStorage } from "./artifact-storage.js";
29
- /** Canonical filename for a plan artifact. UI detection keys on this name. */
48
+ /**
49
+ * Legacy plan filename. Detection-only: {@link isPlanArtifactName} still
50
+ * accepts this exact name so plans published before named artifacts existed
51
+ * keep working. It is NEVER freshly emitted — a titleless plan now falls back
52
+ * to a bare `<hash>.plan.md` (see {@link planArtifactName}).
53
+ */
30
54
  export declare const PLAN_ARTIFACT_NAME = "plan.md";
31
55
  /**
32
- * Sandbox path recorded on the artifact. Routes under `.stigmer/` (the
33
- * session platform dir), so it never pollutes the user's workspace, and a
34
- * follow-up execution can reference it via workspace file refs if desired.
56
+ * Suffix every named plan artifact carries. The UI keys plan detection on this
57
+ * suffix, so it must stay in sync with the SDK's `PLAN_ARTIFACT_SUFFIX`
58
+ * (`sdk/react/src/library/detect-plan-artifact.ts`). The two cannot share a
59
+ * module — the runner and the browser SDK have disjoint module graphs — so the
60
+ * constant is duplicated by design, mirroring {@link PLAN_ARTIFACT_NAME}.
61
+ */
62
+ export declare const PLAN_ARTIFACT_SUFFIX = ".plan.md";
63
+ /**
64
+ * Reports whether an artifact filename is a plan: the legacy exact name, or any
65
+ * `*.plan.md`. Kept in sync with the SDK's `isPlanArtifactName`.
66
+ */
67
+ export declare function isPlanArtifactName(name: string): boolean;
68
+ /**
69
+ * Derives the plan artifact's filename from its text: `<slug>_<id>.plan.md`
70
+ * when the plan opens with a titled `# H1`, else a bare `<id>.plan.md`. The
71
+ * `<id>` is the first {@link PLAN_ID_LENGTH} hex of the content's SHA-256 — a
72
+ * user-facing discriminator for the shared download/list basename namespace,
73
+ * not a storage-uniqueness device (see the module doc).
74
+ */
75
+ export declare function planArtifactName(planText: string): string;
76
+ /**
77
+ * Sandbox path recorded on the artifact for a given filename. Routes under
78
+ * `.stigmer/` (the session platform dir), so it never pollutes the user's
79
+ * workspace, and a follow-up execution can reference it via workspace file
80
+ * refs if desired.
35
81
  */
36
- export declare const PLAN_ARTIFACT_SANDBOX_PATH = ".stigmer/plans/plan.md";
82
+ export declare function planArtifactSandboxPath(name: string): string;
37
83
  /**
38
84
  * Returns the text of the last AI message in a completed status, trimmed.
39
85
  * Returns `undefined` when there is no AI message with content — the plan was
@@ -41,9 +87,11 @@ export declare const PLAN_ARTIFACT_SANDBOX_PATH = ".stigmer/plans/plan.md";
41
87
  */
42
88
  export declare function extractFinalPlanText(status: AgentExecutionStatus): string | undefined;
43
89
  /**
44
- * Publishes `planText` as a `plan.md` ExecutionArtifact and registers it on
45
- * `status.artifacts`. Idempotent: re-publishing replaces any existing `plan.md`
46
- * rather than appending a duplicate, preserving a single source of truth.
90
+ * Publishes `planText` as a plan ExecutionArtifact (named from its title —
91
+ * see {@link planArtifactName}) and registers it on `status.artifacts`.
92
+ * Idempotent: re-publishing replaces any existing plan artifact matched by
93
+ * {@link isPlanArtifactName}, not exact name, so a re-plan whose title changed
94
+ * still supersedes rather than appends — preserving a single source of truth.
47
95
  *
48
96
  * Fire-and-forget by contract: a plan that fails to upload must never fail the
49
97
  * execution. Errors are logged and swallowed.
@@ -2,11 +2,30 @@
2
2
  * Plan-mode artifact publishing.
3
3
  *
4
4
  * When an execution runs in Plan mode (InteractionMode.PLAN), the agent's final
5
- * message IS the plan. We publish that text as a first-class `plan.md`
5
+ * message IS the plan. We publish that text as a first-class plan markdown
6
6
  * ExecutionArtifact so the UI can render a reviewable Plan card with
7
7
  * copy/download, and a follow-up "Implement" execution can reference it
8
8
  * deterministically.
9
9
  *
10
+ * The artifact is named from the plan's own title — a hyphenated slug of its
11
+ * leading `# H1`, a `_<hash>` discriminator, and a `.plan.md` suffix (e.g.
12
+ * `plan-card-ux-cleanup_a1b2c3d4.plan.md`), so a downloaded plan lands as a
13
+ * recognizable file and the card, plan tab, and saved file all agree on one
14
+ * name. A plan with no derivable title falls back to a bare `<hash>.plan.md`.
15
+ *
16
+ * The `_<hash>` discriminator (first 8 hex of the plan content's SHA-256) is
17
+ * NOT for storage uniqueness — storage keys are already execution-scoped
18
+ * (`artifacts/{execId}/<name>`). It exists because the artifact BASENAME is a
19
+ * user-facing shared namespace: downloads save under it (see the artifact
20
+ * download disposition) and the artifact list surfaces it, so two same-titled
21
+ * plans would otherwise collide in the user's Downloads folder and read
22
+ * identically in the list. Deriving it from content (not a random or
23
+ * execution-scoped value) keeps naming honestly idempotent — identical content
24
+ * yields an identical name, so a finalize retry re-uploads to the same key,
25
+ * while any real edit yields a distinct one. This refines DD-23 §D3 ("no
26
+ * uniqueness hash"), which was correct about storage but overlooked the
27
+ * download/list basename namespace introduced by DD-23 §D1.
28
+ *
10
29
  * This is deliberately a single, harness-agnostic helper:
11
30
  * - The native (deepagents) harness already auto-publishes files an agent
12
31
  * writes (InlinePublisher), but Plan mode is read-only, so there is no file to
@@ -21,22 +40,128 @@
21
40
  *
22
41
  * The plan content is NOT duplicated as a separate stored blob beyond this
23
42
  * artifact — the chat message remains the live/streamed view; the artifact is
24
- * the durable/exportable view, detected by convention (a FILE artifact named
25
- * `plan.md`).
43
+ * the durable/exportable view, detected by convention (a FILE artifact whose
44
+ * name satisfies {@link isPlanArtifactName}).
26
45
  */
27
46
  import { createHash } from "node:crypto";
28
47
  import { create } from "@bufbuild/protobuf";
29
48
  import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
30
49
  import { ExecutionArtifactKind, MessageType, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
31
50
  import { utcTimestamp } from "./status.js";
32
- /** Canonical filename for a plan artifact. UI detection keys on this name. */
51
+ /**
52
+ * Legacy plan filename. Detection-only: {@link isPlanArtifactName} still
53
+ * accepts this exact name so plans published before named artifacts existed
54
+ * keep working. It is NEVER freshly emitted — a titleless plan now falls back
55
+ * to a bare `<hash>.plan.md` (see {@link planArtifactName}).
56
+ */
33
57
  export const PLAN_ARTIFACT_NAME = "plan.md";
34
58
  /**
35
- * Sandbox path recorded on the artifact. Routes under `.stigmer/` (the
36
- * session platform dir), so it never pollutes the user's workspace, and a
37
- * follow-up execution can reference it via workspace file refs if desired.
59
+ * Suffix every named plan artifact carries. The UI keys plan detection on this
60
+ * suffix, so it must stay in sync with the SDK's `PLAN_ARTIFACT_SUFFIX`
61
+ * (`sdk/react/src/library/detect-plan-artifact.ts`). The two cannot share a
62
+ * module — the runner and the browser SDK have disjoint module graphs — so the
63
+ * constant is duplicated by design, mirroring {@link PLAN_ARTIFACT_NAME}.
38
64
  */
39
- export const PLAN_ARTIFACT_SANDBOX_PATH = ".stigmer/plans/plan.md";
65
+ export const PLAN_ARTIFACT_SUFFIX = ".plan.md";
66
+ /** Longest slug we derive from a plan title before the `_<id>.plan.md` tail. */
67
+ const MAX_PLAN_SLUG_LENGTH = 60;
68
+ /** Hex length of the content-hash discriminator appended to named plans. */
69
+ const PLAN_ID_LENGTH = 8;
70
+ /**
71
+ * Reports whether an artifact filename is a plan: the legacy exact name, or any
72
+ * `*.plan.md`. Kept in sync with the SDK's `isPlanArtifactName`.
73
+ */
74
+ export function isPlanArtifactName(name) {
75
+ return name === PLAN_ARTIFACT_NAME || name.endsWith(PLAN_ARTIFACT_SUFFIX);
76
+ }
77
+ // Plan-title extraction, mirrored from the SDK so the runner's derived filename
78
+ // and the card's displayed title agree on one title from one source (the plan's
79
+ // leading `# H1`). These three patterns are a verbatim copy of
80
+ // `sdk/react/src/internal/markdown-components.tsx`
81
+ // (`ENCLOSING_MARKDOWN_FENCE_RE`, `ENCLOSING_BARE_FENCE_RE`, `LEADING_H1_RE`)
82
+ // and MUST stay in sync with it — a fence-wrapped plan whose title the card
83
+ // unwraps must slug to that same title here, never to the fallback.
84
+ const ENCLOSING_MARKDOWN_FENCE_RE = /^(`{3,})[ \t]*(?:markdown|md)[ \t]*\r?\n([\s\S]*?)\r?\n\1[ \t]*$/i;
85
+ const ENCLOSING_BARE_FENCE_RE = /^(`{3,})[ \t]*\r?\n([\s\S]*?)\r?\n\1[ \t]*$/;
86
+ const LEADING_H1_RE = /^#[ \t]+(.+?)[ \t]*(?:\r?\n+|$)/;
87
+ /**
88
+ * The plan's title: the leading `# H1` of the plan text, after the same
89
+ * plan-scoped enclosing-fence unwrap (tagged ```markdown``` or a bare ``` `)
90
+ * the document renderers apply. `undefined` when the plan has no leading H1.
91
+ */
92
+ function extractPlanTitle(planText) {
93
+ const trimmed = planText.trim();
94
+ let body = trimmed;
95
+ const tagged = ENCLOSING_MARKDOWN_FENCE_RE.exec(trimmed);
96
+ if (tagged) {
97
+ body = tagged[2];
98
+ }
99
+ else {
100
+ const bare = ENCLOSING_BARE_FENCE_RE.exec(trimmed);
101
+ if (bare)
102
+ body = bare[2];
103
+ }
104
+ const h1 = LEADING_H1_RE.exec(body.trim());
105
+ return h1 ? h1[1] : undefined;
106
+ }
107
+ /**
108
+ * Strips a leading "Plan" LABEL from a title, e.g. `Plan: Create X` -> `Create
109
+ * X`. The separator (`:` or a dash) is REQUIRED: this removes a redundant label
110
+ * (a plan document already announces itself via the `.plan.md` suffix and the
111
+ * Plan card framing) without ever clipping a real title word — a bare `\bplan\b`
112
+ * would wrongly turn "Plan card UX cleanup" into "card UX cleanup".
113
+ *
114
+ * Deliberately runner/filename-only and NOT mirrored into the SDK's title
115
+ * extraction: display surfaces render the message's own `# H1` faithfully
116
+ * (`extractLeadingH1` is general-purpose and render-time-only), so the clean
117
+ * title is fixed at the source — the plan-mode prompt tells the model not to
118
+ * prefix titles with "Plan:". This strip is the durable-artifact safety net for
119
+ * when the model (or the enforcement-less Cursor harness) drifts.
120
+ */
121
+ function stripPlanLabel(title) {
122
+ return title.replace(/^plan\s*[:\u2013\u2014-]\s*/i, "");
123
+ }
124
+ /**
125
+ * Slugifies a plan title into a filename-safe stem: lowercase, every run of
126
+ * non-alphanumerics collapsed to `-`, trimmed of leading/trailing `-`, and
127
+ * capped at {@link MAX_PLAN_SLUG_LENGTH}. Returns `""` for a title with no
128
+ * alphanumerics (e.g. only punctuation), which selects the fallback name.
129
+ */
130
+ function slugifyPlanTitle(title) {
131
+ return title
132
+ .toLowerCase()
133
+ .replace(/[^a-z0-9]+/g, "-")
134
+ .replace(/^-+|-+$/g, "")
135
+ .slice(0, MAX_PLAN_SLUG_LENGTH)
136
+ .replace(/-+$/g, "");
137
+ }
138
+ /**
139
+ * Derives the plan artifact's filename from its text: `<slug>_<id>.plan.md`
140
+ * when the plan opens with a titled `# H1`, else a bare `<id>.plan.md`. The
141
+ * `<id>` is the first {@link PLAN_ID_LENGTH} hex of the content's SHA-256 — a
142
+ * user-facing discriminator for the shared download/list basename namespace,
143
+ * not a storage-uniqueness device (see the module doc).
144
+ */
145
+ export function planArtifactName(planText) {
146
+ const id = createHash("sha256")
147
+ .update(planText, "utf-8")
148
+ .digest("hex")
149
+ .slice(0, PLAN_ID_LENGTH);
150
+ const title = extractPlanTitle(planText);
151
+ const slug = title ? slugifyPlanTitle(stripPlanLabel(title)) : "";
152
+ return slug.length > 0
153
+ ? `${slug}_${id}${PLAN_ARTIFACT_SUFFIX}`
154
+ : `${id}${PLAN_ARTIFACT_SUFFIX}`;
155
+ }
156
+ /**
157
+ * Sandbox path recorded on the artifact for a given filename. Routes under
158
+ * `.stigmer/` (the session platform dir), so it never pollutes the user's
159
+ * workspace, and a follow-up execution can reference it via workspace file
160
+ * refs if desired.
161
+ */
162
+ export function planArtifactSandboxPath(name) {
163
+ return `.stigmer/plans/${name}`;
164
+ }
40
165
  /**
41
166
  * Returns the text of the last AI message in a completed status, trimmed.
42
167
  * Returns `undefined` when there is no AI message with content — the plan was
@@ -52,9 +177,11 @@ export function extractFinalPlanText(status) {
52
177
  return undefined;
53
178
  }
54
179
  /**
55
- * Publishes `planText` as a `plan.md` ExecutionArtifact and registers it on
56
- * `status.artifacts`. Idempotent: re-publishing replaces any existing `plan.md`
57
- * rather than appending a duplicate, preserving a single source of truth.
180
+ * Publishes `planText` as a plan ExecutionArtifact (named from its title —
181
+ * see {@link planArtifactName}) and registers it on `status.artifacts`.
182
+ * Idempotent: re-publishing replaces any existing plan artifact matched by
183
+ * {@link isPlanArtifactName}, not exact name, so a re-plan whose title changed
184
+ * still supersedes rather than appends — preserving a single source of truth.
58
185
  *
59
186
  * Fire-and-forget by contract: a plan that fails to upload must never fail the
60
187
  * execution. Errors are logged and swallowed.
@@ -67,25 +194,26 @@ export async function publishPlanArtifact(opts) {
67
194
  try {
68
195
  const content = Buffer.from(planText, "utf-8");
69
196
  const contentHash = createHash("sha256").update(content).digest("hex");
70
- const storageKey = `artifacts/${executionId}/${PLAN_ARTIFACT_NAME}`;
197
+ const name = planArtifactName(planText);
198
+ const storageKey = `artifacts/${executionId}/${name}`;
71
199
  await artifactStorage.upload(storageKey, content, "text/markdown");
72
200
  const artifact = create(ExecutionArtifactSchema, {
73
- name: PLAN_ARTIFACT_NAME,
74
- sandboxPath: PLAN_ARTIFACT_SANDBOX_PATH,
201
+ name,
202
+ sandboxPath: planArtifactSandboxPath(name),
75
203
  kind: ExecutionArtifactKind.FILE,
76
204
  sizeBytes: BigInt(content.length),
77
205
  storageKey,
78
206
  createdAt: utcTimestamp(),
79
207
  contentHash,
80
208
  });
81
- const existingIdx = status.artifacts.findIndex((a) => a.name === PLAN_ARTIFACT_NAME);
209
+ const existingIdx = status.artifacts.findIndex((a) => isPlanArtifactName(a.name));
82
210
  if (existingIdx >= 0) {
83
211
  status.artifacts[existingIdx] = artifact;
84
212
  }
85
213
  else {
86
214
  status.artifacts.push(artifact);
87
215
  }
88
- console.log(`[plan-artifact] execution=${executionId} — published ${PLAN_ARTIFACT_NAME} ` +
216
+ console.log(`[plan-artifact] execution=${executionId} — published ${name} ` +
89
217
  `(${content.length} bytes, hash=${contentHash.slice(0, 12)})`);
90
218
  }
91
219
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"plan-artifact.js","sourceRoot":"","sources":["../../src/shared/plan-artifact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,uBAAuB,EAAE,MAAM,kEAAkE,CAAC;AAC3G,OAAO,EACL,qBAAqB,EACrB,WAAW,GACZ,MAAM,8DAA8D,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,wBAAwB,CAAC;AAEnE;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAA4B;IAC/D,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACrD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC/B,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,CAAC,UAAU,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzE,OAAO,GAAG,CAAC,OAAO,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,IAKzC;IACC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC;IAEhE,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC/C,MAAM,WAAW,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,MAAM,UAAU,GAAG,aAAa,WAAW,IAAI,kBAAkB,EAAE,CAAC;QAEpE,MAAM,eAAe,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC;QAEnE,MAAM,QAAQ,GAAG,MAAM,CAAC,uBAAuB,EAAE;YAC/C,IAAI,EAAE,kBAAkB;YACxB,WAAW,EAAE,0BAA0B;YACvC,IAAI,EAAE,qBAAqB,CAAC,IAAI;YAChC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YACjC,UAAU;YACV,SAAS,EAAE,YAAY,EAAE;YACzB,WAAW;SACZ,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC;QACrF,IAAI,WAAW,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,QAAQ,CAAC;QAC3C,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;QAED,OAAO,CAAC,GAAG,CACT,6BAA6B,WAAW,gBAAgB,kBAAkB,GAAG;YAC7E,IAAI,OAAO,CAAC,MAAM,gBAAgB,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAC9D,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CACV,6BAA6B,WAAW,KAAK;YAC7C,uCAAuC,GAAG,EAAE,CAC7C,CAAC;IACJ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"plan-artifact.js","sourceRoot":"","sources":["../../src/shared/plan-artifact.ts"],"names":[],"mappings":"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"}