@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.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +29 -0
- package/dist/activities/execute-cursor/capture-flow.js +59 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/command-provenance.d.ts +11 -25
- package/dist/activities/execute-cursor/command-provenance.js +25 -115
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +358 -557
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/todo-tracker.d.ts +6 -1
- package/dist/activities/execute-cursor/todo-tracker.js +15 -43
- package/dist/activities/execute-cursor/todo-tracker.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.d.ts +108 -0
- package/dist/activities/execute-cursor/turn-boundary.js +163 -0
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -0
- package/dist/activities/execute-cursor/turn-stream.d.ts +141 -0
- package/dist/activities/execute-cursor/turn-stream.js +249 -0
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -0
- package/dist/activities/execute-deep-agent/command-provenance.d.ts +61 -0
- package/dist/activities/execute-deep-agent/command-provenance.js +72 -0
- package/dist/activities/execute-deep-agent/command-provenance.js.map +1 -0
- package/dist/activities/execute-deep-agent/index.js +73 -4
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +8 -1
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js +6 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +7 -0
- package/dist/activities/execute-deep-agent/streaming.js +5 -1
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +12 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/otel.js +10 -0
- package/dist/otel.js.map +1 -1
- package/dist/shared/filereview/capture.d.ts +18 -1
- package/dist/shared/filereview/capture.js +105 -28
- package/dist/shared/filereview/capture.js.map +1 -1
- package/dist/shared/filereview/cas-progress.d.ts +63 -0
- package/dist/shared/filereview/cas-progress.js +128 -0
- package/dist/shared/filereview/cas-progress.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +38 -0
- package/dist/shared/filereview/cas-substrate.js +48 -15
- package/dist/shared/filereview/cas-substrate.js.map +1 -1
- package/dist/shared/filereview/command-provenance.d.ts +93 -0
- package/dist/shared/filereview/command-provenance.js +132 -0
- package/dist/shared/filereview/command-provenance.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +16 -0
- package/dist/shared/filereview/events.js +37 -0
- package/dist/shared/filereview/events.js.map +1 -1
- package/dist/shared/filereview/git-substrate.d.ts +48 -0
- package/dist/shared/filereview/git-substrate.js +78 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -1
- package/dist/shared/filereview/index.d.ts +5 -2
- package/dist/shared/filereview/index.js +5 -2
- package/dist/shared/filereview/index.js.map +1 -1
- package/dist/shared/filereview/line-counts.d.ts +48 -0
- package/dist/shared/filereview/line-counts.js +72 -0
- package/dist/shared/filereview/line-counts.js.map +1 -0
- package/dist/shared/filereview/progress.d.ts +167 -0
- package/dist/shared/filereview/progress.js +196 -0
- package/dist/shared/filereview/progress.js.map +1 -0
- package/dist/shared/implement-plan-prompt.d.ts +6 -4
- package/dist/shared/implement-plan-prompt.js +38 -6
- package/dist/shared/implement-plan-prompt.js.map +1 -1
- package/dist/shared/plan-artifact.d.ts +59 -11
- package/dist/shared/plan-artifact.js +144 -16
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/plan-mode-prompt.d.ts +15 -4
- package/dist/shared/plan-mode-prompt.js +28 -5
- package/dist/shared/plan-mode-prompt.js.map +1 -1
- package/dist/shared/todos.d.ts +56 -0
- package/dist/shared/todos.js +98 -0
- package/dist/shared/todos.js.map +1 -0
- package/dist/shared/tool-row.d.ts +16 -0
- package/dist/shared/tool-row.js +31 -0
- package/dist/shared/tool-row.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/otel-turn-span.test.ts +61 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -0
- package/src/activities/execute-cursor/__tests__/delta-enricher.test.ts +68 -0
- package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
- package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +280 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +349 -0
- package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
- package/src/activities/execute-cursor/capture-flow.ts +78 -0
- package/src/activities/execute-cursor/command-provenance.ts +25 -120
- package/src/activities/execute-cursor/index.ts +419 -602
- package/src/activities/execute-cursor/todo-tracker.ts +17 -59
- package/src/activities/execute-cursor/turn-boundary.ts +274 -0
- package/src/activities/execute-cursor/turn-stream.ts +418 -0
- package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +252 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +11 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +78 -0
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +105 -1
- package/src/activities/execute-deep-agent/command-provenance.ts +102 -0
- package/src/activities/execute-deep-agent/index.ts +91 -4
- package/src/activities/execute-deep-agent/status-builder.ts +9 -0
- package/src/activities/execute-deep-agent/streaming-v3.ts +6 -1
- package/src/activities/execute-deep-agent/streaming.ts +12 -1
- package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
- package/src/otel.ts +8 -0
- package/src/shared/__tests__/implement-plan-prompt.test.ts +40 -2
- package/src/shared/__tests__/plan-artifact.test.ts +142 -9
- package/src/shared/__tests__/todos.test.ts +216 -0
- package/src/shared/filereview/__tests__/capture.test.ts +182 -1
- package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +100 -0
- package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
- package/src/shared/filereview/__tests__/events.test.ts +105 -0
- package/src/shared/filereview/__tests__/line-counts.test.ts +73 -0
- package/src/shared/filereview/__tests__/progress.test.ts +338 -0
- package/src/shared/filereview/capture.ts +134 -30
- package/src/shared/filereview/cas-progress.ts +170 -0
- package/src/shared/filereview/cas-substrate.ts +79 -18
- package/src/shared/filereview/command-provenance.ts +180 -0
- package/src/shared/filereview/events.ts +50 -0
- package/src/shared/filereview/git-substrate.ts +114 -0
- package/src/shared/filereview/index.ts +36 -1
- package/src/shared/filereview/line-counts.ts +83 -0
- package/src/shared/filereview/progress.ts +310 -0
- package/src/shared/implement-plan-prompt.ts +39 -8
- package/src/shared/plan-artifact.ts +152 -16
- package/src/shared/plan-mode-prompt.ts +28 -5
- package/src/shared/todos.ts +126 -0
- package/src/shared/tool-row.ts +34 -0
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
|
|
39
|
+
import { create } from "@bufbuild/protobuf";
|
|
40
|
+
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
41
|
+
import type { FileChangeKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
42
|
+
import type {
|
|
43
|
+
FileChangeProgress,
|
|
44
|
+
FileChangeProgressEntry,
|
|
45
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
46
|
+
import {
|
|
47
|
+
FileChangeProgressEntrySchema,
|
|
48
|
+
FileChangeProgressSchema,
|
|
49
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
50
|
+
import { utcTimestamp } from "../status.js";
|
|
51
|
+
import { toFileChangeKind } from "./capture.js";
|
|
52
|
+
import {
|
|
53
|
+
captureProgressDelta,
|
|
54
|
+
type GitProgressEntry,
|
|
55
|
+
} from "./git-substrate.js";
|
|
56
|
+
import { isSecretLikePath } from "./secret-paths.js";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One file's slim, content-free progress row — the substrate-neutral input to
|
|
60
|
+
* {@link buildFileChangeProgress}. Carries the proto {@link FileChangeKind}
|
|
61
|
+
* directly (git and cas substrates both normalize to it), so no side re-maps.
|
|
62
|
+
* `linesAdded`/`linesRemoved` are 0 when uncountable (binary / oversized /
|
|
63
|
+
* secret-withheld).
|
|
64
|
+
*/
|
|
65
|
+
export interface ProgressEntry {
|
|
66
|
+
/** Path before the change (workspace-relative). Empty for ADD. */
|
|
67
|
+
readonly pathBefore: string;
|
|
68
|
+
/** Path after the change (workspace-relative). Empty for DELETE. */
|
|
69
|
+
readonly pathAfter: string;
|
|
70
|
+
readonly kind: FileChangeKind;
|
|
71
|
+
readonly linesAdded: number;
|
|
72
|
+
readonly linesRemoved: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The substrate-neutral progress delta the git/cas/hybrid substrates emit.
|
|
77
|
+
*
|
|
78
|
+
* `totalFilesChanged` is the honest count of changed files this turn, which may
|
|
79
|
+
* EXCEED `entries.length` when a substrate caps how many files it reads (the cas
|
|
80
|
+
* substrate reads only a bounded prefix — DD-33). Undefined means "the entries
|
|
81
|
+
* ARE every changed file" (the git substrate, whose numstat is free), so
|
|
82
|
+
* {@link buildFileChangeProgress} falls back to `entries.length`.
|
|
83
|
+
*/
|
|
84
|
+
export interface ProgressDelta {
|
|
85
|
+
readonly entries: readonly ProgressEntry[];
|
|
86
|
+
readonly totalFilesChanged?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** One mid-run capture: the FULL cumulative turn delta + whether it changed. */
|
|
90
|
+
export interface ProgressCapture {
|
|
91
|
+
readonly delta: ProgressDelta;
|
|
92
|
+
/** False ⇒ nothing moved since the last capture; the caller skips re-attach. */
|
|
93
|
+
readonly changed: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A per-turn source of the mid-run delta. Implementations own their own
|
|
98
|
+
* short-circuit cache and ALWAYS return the full cumulative delta (never a
|
|
99
|
+
* bare "unchanged" sentinel), so {@link createHybridProgressSubstrate} can merge
|
|
100
|
+
* a changed slice with an unchanged one without losing the latter.
|
|
101
|
+
*/
|
|
102
|
+
export interface ProgressSubstrate {
|
|
103
|
+
capture(): Promise<ProgressCapture>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Cap the per-file entry list so a turn touching thousands of files does not
|
|
108
|
+
* bloat the persisted status (which rides Temporal / the store). `files_changed`
|
|
109
|
+
* and the aggregate counts stay honest over ALL files; the strip shows "… and K
|
|
110
|
+
* more" when the list is capped. Also the cas substrate's read budget — it reads
|
|
111
|
+
* after-bytes for at most this many files per capture (DD-33).
|
|
112
|
+
*/
|
|
113
|
+
export const PROGRESS_MAX_ENTRIES = 200;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Minimum wall-clock gap between two mid-run captures, in ms. A floor on cost:
|
|
117
|
+
* every capture stages the working tree (git) or reads the touched set (cas), so
|
|
118
|
+
* we bound how often that runs regardless of persist frequency. Env-tunable for
|
|
119
|
+
* large-repo deployments; defaults to 2s (feels live without hammering the disk).
|
|
120
|
+
*/
|
|
121
|
+
export const PROGRESS_CAPTURE_MIN_INTERVAL_MS = readMinIntervalMs();
|
|
122
|
+
|
|
123
|
+
function readMinIntervalMs(): number {
|
|
124
|
+
const raw = process.env.STIGMER_PROGRESS_CAPTURE_MIN_INTERVAL_MS;
|
|
125
|
+
if (!raw) return 2000;
|
|
126
|
+
const n = Number.parseInt(raw, 10);
|
|
127
|
+
return Number.isFinite(n) && n >= 0 ? n : 2000;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether enough wall-clock has elapsed since the last capture to take another.
|
|
132
|
+
* Pure and separated for direct testing (mirrors `persist-decision.ts`). A
|
|
133
|
+
* `lastAtMs` of 0 (never captured) always passes.
|
|
134
|
+
*/
|
|
135
|
+
export function shouldCaptureProgress(
|
|
136
|
+
lastAtMs: number,
|
|
137
|
+
nowMs: number,
|
|
138
|
+
minIntervalMs: number = PROGRESS_CAPTURE_MIN_INTERVAL_MS,
|
|
139
|
+
): boolean {
|
|
140
|
+
return nowMs - lastAtMs >= minIntervalMs;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build the transient {@link FileChangeProgress} message from a substrate-neutral
|
|
145
|
+
* delta. Zeroes counts for secret-like paths (path visible, magnitude withheld),
|
|
146
|
+
* caps the entry list at {@link PROGRESS_MAX_ENTRIES}, and reports the honest
|
|
147
|
+
* `files_changed` (`delta.totalFilesChanged` when a substrate capped its reads,
|
|
148
|
+
* else the entry count). Aggregate counts sum over the emitted entries. Pure and
|
|
149
|
+
* exported for direct testing.
|
|
150
|
+
*/
|
|
151
|
+
export function buildFileChangeProgress(
|
|
152
|
+
delta: ProgressDelta,
|
|
153
|
+
changeSetId: string,
|
|
154
|
+
): FileChangeProgress {
|
|
155
|
+
let totalAdded = 0;
|
|
156
|
+
let totalRemoved = 0;
|
|
157
|
+
const entries: FileChangeProgressEntry[] = [];
|
|
158
|
+
|
|
159
|
+
for (const entry of delta.entries) {
|
|
160
|
+
// A secret-like path is surfaced (path visible) but its magnitude is withheld
|
|
161
|
+
// — counts zeroed, mirroring the ledger's "path visible, content withheld".
|
|
162
|
+
const secret = isSecretLikePath(entry.pathAfter || entry.pathBefore);
|
|
163
|
+
const linesAdded = secret ? 0 : entry.linesAdded;
|
|
164
|
+
const linesRemoved = secret ? 0 : entry.linesRemoved;
|
|
165
|
+
totalAdded += linesAdded;
|
|
166
|
+
totalRemoved += linesRemoved;
|
|
167
|
+
|
|
168
|
+
if (entries.length < PROGRESS_MAX_ENTRIES) {
|
|
169
|
+
entries.push(
|
|
170
|
+
create(FileChangeProgressEntrySchema, {
|
|
171
|
+
pathBefore: entry.pathBefore,
|
|
172
|
+
pathAfter: entry.pathAfter,
|
|
173
|
+
kind: entry.kind,
|
|
174
|
+
linesAdded,
|
|
175
|
+
linesRemoved,
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return create(FileChangeProgressSchema, {
|
|
182
|
+
changeSetId,
|
|
183
|
+
filesChanged: delta.totalFilesChanged ?? delta.entries.length,
|
|
184
|
+
linesAdded: totalAdded,
|
|
185
|
+
linesRemoved: totalRemoved,
|
|
186
|
+
entries,
|
|
187
|
+
capturedAt: utcTimestamp(),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Mutable per-turn state the caller threads across persists: the last capture
|
|
193
|
+
* time (the floor). Substrate-specific short-circuit state (git tree sha, cas
|
|
194
|
+
* signature) lives inside the {@link ProgressSubstrate}, not here. A fresh turn
|
|
195
|
+
* starts a fresh state (`{ lastAtMs: 0 }`).
|
|
196
|
+
*/
|
|
197
|
+
export interface ProgressCaptureState {
|
|
198
|
+
lastAtMs: number;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** A fresh per-turn progress state. */
|
|
202
|
+
export function newProgressCaptureState(): ProgressCaptureState {
|
|
203
|
+
return { lastAtMs: 0 };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Capture-and-attach the live progress snapshot onto `status.file_change_progress`,
|
|
208
|
+
* throttled by the floor. Mutates `status` and `state` in place; a no-op when the
|
|
209
|
+
* floor has not elapsed or the substrate reports nothing changed. Content-free and
|
|
210
|
+
* secret-safe (see {@link buildFileChangeProgress}). Called by both harnesses from
|
|
211
|
+
* their per-persist loop, each passing a substrate built for its workspace shape.
|
|
212
|
+
*
|
|
213
|
+
* The field is set even when the delta is now EMPTY (the agent reverted its own
|
|
214
|
+
* edits) so the strip reflects the reversion (it hides at zero) rather than
|
|
215
|
+
* showing a stale count — the server's presence-guarded merge would keep the
|
|
216
|
+
* stale value if the runner omitted the field.
|
|
217
|
+
*/
|
|
218
|
+
export async function captureFileChangeProgress(opts: {
|
|
219
|
+
readonly status: AgentExecutionStatus;
|
|
220
|
+
readonly changeSetId: string;
|
|
221
|
+
readonly substrate: ProgressSubstrate;
|
|
222
|
+
readonly state: ProgressCaptureState;
|
|
223
|
+
/** Injectable clock for tests; defaults to `Date.now()`. */
|
|
224
|
+
readonly nowMs?: number;
|
|
225
|
+
}): Promise<void> {
|
|
226
|
+
const now = opts.nowMs ?? Date.now();
|
|
227
|
+
if (!shouldCaptureProgress(opts.state.lastAtMs, now)) return;
|
|
228
|
+
opts.state.lastAtMs = now;
|
|
229
|
+
|
|
230
|
+
const { delta, changed } = await opts.substrate.capture();
|
|
231
|
+
if (!changed) return;
|
|
232
|
+
|
|
233
|
+
opts.status.fileChangeProgress = buildFileChangeProgress(delta, opts.changeSetId);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Substrates
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
function gitEntryToProgressEntry(e: GitProgressEntry): ProgressEntry {
|
|
241
|
+
return {
|
|
242
|
+
pathBefore: e.pathBefore,
|
|
243
|
+
pathAfter: e.pathAfter,
|
|
244
|
+
kind: toFileChangeKind(e.changeType),
|
|
245
|
+
linesAdded: e.linesAdded,
|
|
246
|
+
linesRemoved: e.linesRemoved,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The git substrate: the working-tree `--numstat` delta against the pinned
|
|
252
|
+
* baseline. `git add -A` honors `.gitignore`, so this covers exactly the
|
|
253
|
+
* git-TRACKED changes (from any source — tool or shell), disjoint from the cas
|
|
254
|
+
* substrate's gitignored set. Preserves the tree-sha short-circuit: on an
|
|
255
|
+
* unchanged tree it returns the cached full delta with `changed:false` (the
|
|
256
|
+
* caller skips the re-attach), byte-identical to the pre-DD-33 behavior.
|
|
257
|
+
*/
|
|
258
|
+
export function createGitProgressSubstrate(opts: {
|
|
259
|
+
readonly workspaceRoot: string;
|
|
260
|
+
readonly executionId: string;
|
|
261
|
+
readonly baselineTree: string;
|
|
262
|
+
readonly excludePaths?: readonly string[];
|
|
263
|
+
}): ProgressSubstrate {
|
|
264
|
+
let lastTreeSha: string | undefined;
|
|
265
|
+
let cachedFull: ProgressDelta = { entries: [] };
|
|
266
|
+
return {
|
|
267
|
+
async capture(): Promise<ProgressCapture> {
|
|
268
|
+
const gitDelta = await captureProgressDelta(
|
|
269
|
+
opts.workspaceRoot,
|
|
270
|
+
opts.executionId,
|
|
271
|
+
opts.baselineTree,
|
|
272
|
+
opts.excludePaths,
|
|
273
|
+
lastTreeSha,
|
|
274
|
+
);
|
|
275
|
+
if (gitDelta === undefined) {
|
|
276
|
+
// Working tree unchanged since the last capture — reuse the cache so the
|
|
277
|
+
// hybrid can still merge this (unchanged) slice with a changed cas slice.
|
|
278
|
+
return { delta: cachedFull, changed: false };
|
|
279
|
+
}
|
|
280
|
+
lastTreeSha = gitDelta.afterTree;
|
|
281
|
+
cachedFull = { entries: gitDelta.entries.map(gitEntryToProgressEntry) };
|
|
282
|
+
return { delta: cachedFull, changed: true };
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The hybrid substrate: a git tree whose gitignored writes are captured via CAS.
|
|
289
|
+
* Concatenates both slices (disjoint by the `.gitignore` boundary — numstat sees
|
|
290
|
+
* only tracked paths, the observer only gitignored ones) and sums the honest
|
|
291
|
+
* totals. `changed` is true when EITHER slice moved; because each child returns
|
|
292
|
+
* its full cumulative delta, the merged delta always carries both slices.
|
|
293
|
+
*/
|
|
294
|
+
export function createHybridProgressSubstrate(
|
|
295
|
+
git: ProgressSubstrate,
|
|
296
|
+
cas: ProgressSubstrate,
|
|
297
|
+
): ProgressSubstrate {
|
|
298
|
+
return {
|
|
299
|
+
async capture(): Promise<ProgressCapture> {
|
|
300
|
+
const [g, c] = await Promise.all([git.capture(), cas.capture()]);
|
|
301
|
+
const delta: ProgressDelta = {
|
|
302
|
+
entries: [...g.delta.entries, ...c.delta.entries],
|
|
303
|
+
totalFilesChanged:
|
|
304
|
+
(g.delta.totalFilesChanged ?? g.delta.entries.length) +
|
|
305
|
+
(c.delta.totalFilesChanged ?? c.delta.entries.length),
|
|
306
|
+
};
|
|
307
|
+
return { delta, changed: g.changed || c.changed };
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -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
|
|
15
|
-
* The directive points the model at
|
|
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
|
|
@@ -24,12 +25,37 @@
|
|
|
24
25
|
* harness).
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
|
-
import {
|
|
28
|
+
import { isPlanArtifactName } from "./plan-artifact.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Progress-tracking instruction shared by both directive variants (Tier 3 of
|
|
32
|
+
* Plan mode — plan-driven build progress). The agent's own to-do tool is the
|
|
33
|
+
* single writer of `status.todos`, so instructing it to derive the list from
|
|
34
|
+
* the approved plan is the entire plan→progress linkage: the runner's todo
|
|
35
|
+
* extraction and the clients' todo renderers light up unchanged.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately tool-agnostic ("your to-do list"): the Cursor harness exposes
|
|
38
|
+
* TodoWrite/updateTodos, the native harness write_todos, and each runtime
|
|
39
|
+
* already teaches the model its own tool.
|
|
40
|
+
*
|
|
41
|
+
* Wording constraint: this block rides BOTH variants, and the conversation-only
|
|
42
|
+
* variant is pinned by tests to never contain "plan.md" (it has no plan file
|
|
43
|
+
* to reference) — so say "the plan", never name the file.
|
|
44
|
+
*/
|
|
45
|
+
const TRACK_PROGRESS_INSTRUCTION = [
|
|
46
|
+
"Track your progress with your to-do list so the user can follow the " +
|
|
47
|
+
"build:",
|
|
48
|
+
"- Before you start, break the plan into a concrete, ordered to-do list — " +
|
|
49
|
+
"roughly one item per implementation step.",
|
|
50
|
+
"- As you work, keep it current: mark each item in progress when you " +
|
|
51
|
+
"begin it and completed when it is done.",
|
|
52
|
+
].join("\n");
|
|
28
53
|
|
|
29
54
|
/**
|
|
30
55
|
* Find the approved plan document among the workspace paths the harness
|
|
31
|
-
* injected for this execution's attachments. Detection keys on the
|
|
32
|
-
*
|
|
56
|
+
* injected for this execution's attachments. Detection keys on the plan
|
|
57
|
+
* filename convention ({@link isPlanArtifactName} — the legacy `plan.md` or any
|
|
58
|
+
* `*.plan.md`), the same convention the UI uses to detect the plan artifact.
|
|
33
59
|
* Returns `undefined` when no plan attachment landed (upload failed, or the
|
|
34
60
|
* attachment itself failed to inject), which selects the conversation-only
|
|
35
61
|
* directive variant.
|
|
@@ -37,9 +63,10 @@ import { PLAN_ARTIFACT_NAME } from "./plan-artifact.js";
|
|
|
37
63
|
export function findApprovedPlanPath(
|
|
38
64
|
attachmentPaths: readonly string[],
|
|
39
65
|
): string | undefined {
|
|
40
|
-
return attachmentPaths.find(
|
|
41
|
-
|
|
42
|
-
|
|
66
|
+
return attachmentPaths.find((p) => {
|
|
67
|
+
const name = p.split("/").pop();
|
|
68
|
+
return name !== undefined && isPlanArtifactName(name);
|
|
69
|
+
});
|
|
43
70
|
}
|
|
44
71
|
|
|
45
72
|
/**
|
|
@@ -61,6 +88,8 @@ export function buildImplementPlanDirective(planPath?: string): string {
|
|
|
61
88
|
"That document is the authoritative version of the plan — the user may " +
|
|
62
89
|
"have edited it after it was proposed, so where it differs from the " +
|
|
63
90
|
"conversation above, follow the document.",
|
|
91
|
+
"",
|
|
92
|
+
TRACK_PROGRESS_INSTRUCTION,
|
|
64
93
|
].join("\n");
|
|
65
94
|
}
|
|
66
95
|
|
|
@@ -69,5 +98,7 @@ export function buildImplementPlanDirective(planPath?: string): string {
|
|
|
69
98
|
"APPROVED.",
|
|
70
99
|
"",
|
|
71
100
|
"Implement the plan proposed in the conversation above, step by step.",
|
|
101
|
+
"",
|
|
102
|
+
TRACK_PROGRESS_INSTRUCTION,
|
|
72
103
|
].join("\n");
|
|
73
104
|
}
|
|
@@ -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
|
|
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,8 +40,8 @@
|
|
|
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
|
|
25
|
-
*
|
|
43
|
+
* the durable/exportable view, detected by convention (a FILE artifact whose
|
|
44
|
+
* name satisfies {@link isPlanArtifactName}).
|
|
26
45
|
*/
|
|
27
46
|
|
|
28
47
|
import { createHash } from "node:crypto";
|
|
@@ -36,15 +55,129 @@ import {
|
|
|
36
55
|
import type { ArtifactStorage } from "./artifact-storage.js";
|
|
37
56
|
import { utcTimestamp } from "./status.js";
|
|
38
57
|
|
|
39
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Legacy plan filename. Detection-only: {@link isPlanArtifactName} still
|
|
60
|
+
* accepts this exact name so plans published before named artifacts existed
|
|
61
|
+
* keep working. It is NEVER freshly emitted — a titleless plan now falls back
|
|
62
|
+
* to a bare `<hash>.plan.md` (see {@link planArtifactName}).
|
|
63
|
+
*/
|
|
40
64
|
export const PLAN_ARTIFACT_NAME = "plan.md";
|
|
41
65
|
|
|
42
66
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
67
|
+
* Suffix every named plan artifact carries. The UI keys plan detection on this
|
|
68
|
+
* suffix, so it must stay in sync with the SDK's `PLAN_ARTIFACT_SUFFIX`
|
|
69
|
+
* (`sdk/react/src/library/detect-plan-artifact.ts`). The two cannot share a
|
|
70
|
+
* module — the runner and the browser SDK have disjoint module graphs — so the
|
|
71
|
+
* constant is duplicated by design, mirroring {@link PLAN_ARTIFACT_NAME}.
|
|
72
|
+
*/
|
|
73
|
+
export const PLAN_ARTIFACT_SUFFIX = ".plan.md";
|
|
74
|
+
|
|
75
|
+
/** Longest slug we derive from a plan title before the `_<id>.plan.md` tail. */
|
|
76
|
+
const MAX_PLAN_SLUG_LENGTH = 60;
|
|
77
|
+
|
|
78
|
+
/** Hex length of the content-hash discriminator appended to named plans. */
|
|
79
|
+
const PLAN_ID_LENGTH = 8;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reports whether an artifact filename is a plan: the legacy exact name, or any
|
|
83
|
+
* `*.plan.md`. Kept in sync with the SDK's `isPlanArtifactName`.
|
|
46
84
|
*/
|
|
47
|
-
export
|
|
85
|
+
export function isPlanArtifactName(name: string): boolean {
|
|
86
|
+
return name === PLAN_ARTIFACT_NAME || name.endsWith(PLAN_ARTIFACT_SUFFIX);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Plan-title extraction, mirrored from the SDK so the runner's derived filename
|
|
90
|
+
// and the card's displayed title agree on one title from one source (the plan's
|
|
91
|
+
// leading `# H1`). These three patterns are a verbatim copy of
|
|
92
|
+
// `sdk/react/src/internal/markdown-components.tsx`
|
|
93
|
+
// (`ENCLOSING_MARKDOWN_FENCE_RE`, `ENCLOSING_BARE_FENCE_RE`, `LEADING_H1_RE`)
|
|
94
|
+
// and MUST stay in sync with it — a fence-wrapped plan whose title the card
|
|
95
|
+
// unwraps must slug to that same title here, never to the fallback.
|
|
96
|
+
const ENCLOSING_MARKDOWN_FENCE_RE =
|
|
97
|
+
/^(`{3,})[ \t]*(?:markdown|md)[ \t]*\r?\n([\s\S]*?)\r?\n\1[ \t]*$/i;
|
|
98
|
+
const ENCLOSING_BARE_FENCE_RE = /^(`{3,})[ \t]*\r?\n([\s\S]*?)\r?\n\1[ \t]*$/;
|
|
99
|
+
const LEADING_H1_RE = /^#[ \t]+(.+?)[ \t]*(?:\r?\n+|$)/;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The plan's title: the leading `# H1` of the plan text, after the same
|
|
103
|
+
* plan-scoped enclosing-fence unwrap (tagged ```markdown``` or a bare ``` `)
|
|
104
|
+
* the document renderers apply. `undefined` when the plan has no leading H1.
|
|
105
|
+
*/
|
|
106
|
+
function extractPlanTitle(planText: string): string | undefined {
|
|
107
|
+
const trimmed = planText.trim();
|
|
108
|
+
let body = trimmed;
|
|
109
|
+
const tagged = ENCLOSING_MARKDOWN_FENCE_RE.exec(trimmed);
|
|
110
|
+
if (tagged) {
|
|
111
|
+
body = tagged[2];
|
|
112
|
+
} else {
|
|
113
|
+
const bare = ENCLOSING_BARE_FENCE_RE.exec(trimmed);
|
|
114
|
+
if (bare) body = bare[2];
|
|
115
|
+
}
|
|
116
|
+
const h1 = LEADING_H1_RE.exec(body.trim());
|
|
117
|
+
return h1 ? h1[1] : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Strips a leading "Plan" LABEL from a title, e.g. `Plan: Create X` -> `Create
|
|
122
|
+
* X`. The separator (`:` or a dash) is REQUIRED: this removes a redundant label
|
|
123
|
+
* (a plan document already announces itself via the `.plan.md` suffix and the
|
|
124
|
+
* Plan card framing) without ever clipping a real title word — a bare `\bplan\b`
|
|
125
|
+
* would wrongly turn "Plan card UX cleanup" into "card UX cleanup".
|
|
126
|
+
*
|
|
127
|
+
* Deliberately runner/filename-only and NOT mirrored into the SDK's title
|
|
128
|
+
* extraction: display surfaces render the message's own `# H1` faithfully
|
|
129
|
+
* (`extractLeadingH1` is general-purpose and render-time-only), so the clean
|
|
130
|
+
* title is fixed at the source — the plan-mode prompt tells the model not to
|
|
131
|
+
* prefix titles with "Plan:". This strip is the durable-artifact safety net for
|
|
132
|
+
* when the model (or the enforcement-less Cursor harness) drifts.
|
|
133
|
+
*/
|
|
134
|
+
function stripPlanLabel(title: string): string {
|
|
135
|
+
return title.replace(/^plan\s*[:\u2013\u2014-]\s*/i, "");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Slugifies a plan title into a filename-safe stem: lowercase, every run of
|
|
140
|
+
* non-alphanumerics collapsed to `-`, trimmed of leading/trailing `-`, and
|
|
141
|
+
* capped at {@link MAX_PLAN_SLUG_LENGTH}. Returns `""` for a title with no
|
|
142
|
+
* alphanumerics (e.g. only punctuation), which selects the fallback name.
|
|
143
|
+
*/
|
|
144
|
+
function slugifyPlanTitle(title: string): string {
|
|
145
|
+
return title
|
|
146
|
+
.toLowerCase()
|
|
147
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
148
|
+
.replace(/^-+|-+$/g, "")
|
|
149
|
+
.slice(0, MAX_PLAN_SLUG_LENGTH)
|
|
150
|
+
.replace(/-+$/g, "");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Derives the plan artifact's filename from its text: `<slug>_<id>.plan.md`
|
|
155
|
+
* when the plan opens with a titled `# H1`, else a bare `<id>.plan.md`. The
|
|
156
|
+
* `<id>` is the first {@link PLAN_ID_LENGTH} hex of the content's SHA-256 — a
|
|
157
|
+
* user-facing discriminator for the shared download/list basename namespace,
|
|
158
|
+
* not a storage-uniqueness device (see the module doc).
|
|
159
|
+
*/
|
|
160
|
+
export function planArtifactName(planText: string): string {
|
|
161
|
+
const id = createHash("sha256")
|
|
162
|
+
.update(planText, "utf-8")
|
|
163
|
+
.digest("hex")
|
|
164
|
+
.slice(0, PLAN_ID_LENGTH);
|
|
165
|
+
const title = extractPlanTitle(planText);
|
|
166
|
+
const slug = title ? slugifyPlanTitle(stripPlanLabel(title)) : "";
|
|
167
|
+
return slug.length > 0
|
|
168
|
+
? `${slug}_${id}${PLAN_ARTIFACT_SUFFIX}`
|
|
169
|
+
: `${id}${PLAN_ARTIFACT_SUFFIX}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Sandbox path recorded on the artifact for a given filename. Routes under
|
|
174
|
+
* `.stigmer/` (the session platform dir), so it never pollutes the user's
|
|
175
|
+
* workspace, and a follow-up execution can reference it via workspace file
|
|
176
|
+
* refs if desired.
|
|
177
|
+
*/
|
|
178
|
+
export function planArtifactSandboxPath(name: string): string {
|
|
179
|
+
return `.stigmer/plans/${name}`;
|
|
180
|
+
}
|
|
48
181
|
|
|
49
182
|
/**
|
|
50
183
|
* Returns the text of the last AI message in a completed status, trimmed.
|
|
@@ -62,9 +195,11 @@ export function extractFinalPlanText(status: AgentExecutionStatus): string | und
|
|
|
62
195
|
}
|
|
63
196
|
|
|
64
197
|
/**
|
|
65
|
-
* Publishes `planText` as a
|
|
66
|
-
*
|
|
67
|
-
*
|
|
198
|
+
* Publishes `planText` as a plan ExecutionArtifact (named from its title —
|
|
199
|
+
* see {@link planArtifactName}) and registers it on `status.artifacts`.
|
|
200
|
+
* Idempotent: re-publishing replaces any existing plan artifact — matched by
|
|
201
|
+
* {@link isPlanArtifactName}, not exact name, so a re-plan whose title changed
|
|
202
|
+
* still supersedes rather than appends — preserving a single source of truth.
|
|
68
203
|
*
|
|
69
204
|
* Fire-and-forget by contract: a plan that fails to upload must never fail the
|
|
70
205
|
* execution. Errors are logged and swallowed.
|
|
@@ -84,13 +219,14 @@ export async function publishPlanArtifact(opts: {
|
|
|
84
219
|
try {
|
|
85
220
|
const content = Buffer.from(planText, "utf-8");
|
|
86
221
|
const contentHash = createHash("sha256").update(content).digest("hex");
|
|
87
|
-
const
|
|
222
|
+
const name = planArtifactName(planText);
|
|
223
|
+
const storageKey = `artifacts/${executionId}/${name}`;
|
|
88
224
|
|
|
89
225
|
await artifactStorage.upload(storageKey, content, "text/markdown");
|
|
90
226
|
|
|
91
227
|
const artifact = create(ExecutionArtifactSchema, {
|
|
92
|
-
name
|
|
93
|
-
sandboxPath:
|
|
228
|
+
name,
|
|
229
|
+
sandboxPath: planArtifactSandboxPath(name),
|
|
94
230
|
kind: ExecutionArtifactKind.FILE,
|
|
95
231
|
sizeBytes: BigInt(content.length),
|
|
96
232
|
storageKey,
|
|
@@ -98,7 +234,7 @@ export async function publishPlanArtifact(opts: {
|
|
|
98
234
|
contentHash,
|
|
99
235
|
});
|
|
100
236
|
|
|
101
|
-
const existingIdx = status.artifacts.findIndex((a) => a.name
|
|
237
|
+
const existingIdx = status.artifacts.findIndex((a) => isPlanArtifactName(a.name));
|
|
102
238
|
if (existingIdx >= 0) {
|
|
103
239
|
status.artifacts[existingIdx] = artifact;
|
|
104
240
|
} else {
|
|
@@ -106,7 +242,7 @@ export async function publishPlanArtifact(opts: {
|
|
|
106
242
|
}
|
|
107
243
|
|
|
108
244
|
console.log(
|
|
109
|
-
`[plan-artifact] execution=${executionId} — published ${
|
|
245
|
+
`[plan-artifact] execution=${executionId} — published ${name} ` +
|
|
110
246
|
`(${content.length} bytes, hash=${contentHash.slice(0, 12)})`,
|
|
111
247
|
);
|
|
112
248
|
} catch (err) {
|
|
@@ -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
|
|
7
|
-
* (`plan-artifact.ts` —
|
|
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
|
*
|
|
@@ -25,8 +26,18 @@
|
|
|
25
26
|
/**
|
|
26
27
|
* Directive body injected into the system prompt of every Plan-mode
|
|
27
28
|
* execution. Deliberately explicit about the deliverable's shape: the final
|
|
28
|
-
* message is published verbatim as
|
|
29
|
-
*
|
|
29
|
+
* message is published verbatim as a plan document (whose filename is derived
|
|
30
|
+
* from the leading `#` title), so a fenced or chat-suffixed plan degrades the
|
|
31
|
+
* reviewable document the user sees.
|
|
32
|
+
*
|
|
33
|
+
* Fence hygiene inside the document is part of the same contract. A plan
|
|
34
|
+
* frequently quotes file content that itself contains fenced code blocks
|
|
35
|
+
* ("insert this section into README.md"); with a same-length outer fence the
|
|
36
|
+
* inner block's closer terminates the outer fence early and corrupts the
|
|
37
|
+
* rendered document — on every client (the react SDK and the terminal
|
|
38
|
+
* renderer alike). And because the plan viewers render top-level ```mermaid
|
|
39
|
+
* fences as diagrams, the directive steers diagrams into the plan body proper
|
|
40
|
+
* rather than leaving them buried, unrendered, inside quoted file content.
|
|
30
41
|
*/
|
|
31
42
|
export const PLAN_MODE_DIRECTIVE = [
|
|
32
43
|
"IMPORTANT: You are in Plan mode — a read-only analysis turn whose " +
|
|
@@ -38,13 +49,25 @@ export const PLAN_MODE_DIRECTIVE = [
|
|
|
38
49
|
"- Only read, search, and analyze.",
|
|
39
50
|
"",
|
|
40
51
|
"Deliverable — your FINAL message IS the plan. It is published verbatim " +
|
|
41
|
-
"as a
|
|
52
|
+
"as a plan document that the user reviews and builds from, so:",
|
|
42
53
|
"- Write it as a complete, well-structured markdown document: start with " +
|
|
43
54
|
"a single `#` title and organize the work under `##` section headings. " +
|
|
44
55
|
"Use lists and tables where they aid scanning.",
|
|
56
|
+
'- Give the `#` title a concise, descriptive name for the work itself; do ' +
|
|
57
|
+
'NOT prefix it with "Plan:" (this document is already a plan — the ' +
|
|
58
|
+
"prefix is redundant and leaks into the plan's filename).",
|
|
45
59
|
"- Reference concrete file paths and describe the specific changes " +
|
|
46
60
|
"planned for each.",
|
|
47
61
|
"- Do NOT wrap the document in a code fence.",
|
|
62
|
+
"- When quoting content that itself contains fenced code blocks (e.g. a " +
|
|
63
|
+
"proposed file section with a code sample inside), open the outer fence " +
|
|
64
|
+
"with MORE backticks than any inner fence (four or more) — a same-length " +
|
|
65
|
+
"inner closer would terminate the outer fence early and corrupt the " +
|
|
66
|
+
"rendered document.",
|
|
67
|
+
"- Fenced ```mermaid blocks at the top level of the document render as " +
|
|
68
|
+
"diagrams in the plan viewer. When a diagram helps communicate the " +
|
|
69
|
+
"design (architecture, flows), include it directly in the plan body — " +
|
|
70
|
+
"not only inside quoted file content, where it stays unrendered source.",
|
|
48
71
|
'- Do NOT end with conversational closers ("Let me know...", "Shall I ' +
|
|
49
72
|
'proceed?") — the next step is the user\'s Build action, and trailing ' +
|
|
50
73
|
"chat would be published as part of the document.",
|