pi-ultracode 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -14
- package/README.zh-CN.md +32 -14
- package/extensions/ultracode.ts +11 -10
- package/package.json +17 -7
- package/src/commands.ts +21 -22
- package/src/depth.ts +38 -0
- package/src/index.ts +9 -1
- package/src/mode.ts +82 -58
- package/src/prompts.ts +65 -32
- package/src/thinking.ts +1 -1
- package/src/workflow/agent-runner.ts +23 -8
- package/src/workflow/agent-types.ts +19 -15
- package/src/workflow/hermetic-git.ts +31 -0
- package/src/workflow/journal.ts +134 -3
- package/src/workflow/parser.ts +254 -38
- package/src/workflow/registry.ts +44 -67
- package/src/workflow/repository-context.ts +76 -0
- package/src/workflow/run-artifacts.ts +60 -14
- package/src/workflow/runtime.ts +380 -180
- package/src/workflow/saved-workflow.ts +37 -0
- package/src/workflow/script-executor.ts +56 -10
- package/src/workflow/script-worker.mjs +65 -19
- package/src/workflow/tool.ts +108 -58
- package/src/workflow/workflow-overlay.ts +4 -4
- package/src/workflow/worktree.ts +1965 -244
- package/types/workflow.d.ts +2 -4
package/src/mode.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ultracode mode controller.
|
|
3
3
|
*
|
|
4
|
-
* Ultracode is a session-scoped
|
|
5
|
-
* -
|
|
4
|
+
* Ultracode is a session-scoped semantic analysis-depth mode. While active, it:
|
|
5
|
+
* - applies the configured mode's default thinking level while preserving the
|
|
6
|
+
* user's previous level,
|
|
6
7
|
* - keeps the `workflow` tool active,
|
|
7
|
-
* - injects
|
|
8
|
-
*
|
|
9
|
-
* - persists its on/off state in session custom entries so it survives
|
|
10
|
-
* reload, resume, fork, and compaction.
|
|
8
|
+
* - injects the configured auto/focused/standard/deep policy on every turn,
|
|
9
|
+
* - persists branch-local mode state across reload, resume, fork, and compaction.
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
isActiveUltracodeMode,
|
|
15
|
+
thinkingLevelForMode,
|
|
16
|
+
type ActiveUltracodeMode,
|
|
17
|
+
type UltracodeModeName,
|
|
18
|
+
} from "./depth.ts";
|
|
14
19
|
import { ULTRACODE_ACTIVE_REMINDER, ULTRACODE_TAGLINE, ultracodeSystemBlock } from "./prompts.ts";
|
|
15
20
|
import {
|
|
16
21
|
LEGACY_ULTRACODE_THINKING_LEVEL,
|
|
@@ -24,7 +29,7 @@ export type { ThinkingLevel } from "./thinking.ts";
|
|
|
24
29
|
export const MODE_ENTRY_TYPE = "ultracode-mode";
|
|
25
30
|
|
|
26
31
|
interface PersistedModeState {
|
|
27
|
-
|
|
32
|
+
mode: UltracodeModeName;
|
|
28
33
|
previousThinking?: ThinkingLevel;
|
|
29
34
|
/** `null` records that the setting was originally absent (Pi defaults to medium). */
|
|
30
35
|
previousDefaultThinking?: ThinkingLevel | null;
|
|
@@ -47,13 +52,13 @@ export interface ThinkingPreferenceStore {
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
export class UltracodeMode {
|
|
50
|
-
private
|
|
55
|
+
private mode: UltracodeModeName = "off";
|
|
51
56
|
private suspended = false;
|
|
52
57
|
private previousThinking: ThinkingLevel | undefined;
|
|
53
58
|
private previousDefaultThinking: ThinkingLevel | null | undefined;
|
|
54
59
|
/** Restore a level later if the current non-reasoning model clamps it to off. */
|
|
55
60
|
private pendingPreviousThinking: ThinkingLevel | undefined;
|
|
56
|
-
/** The level Pi actually applied after clamping the
|
|
61
|
+
/** The level Pi actually applied after clamping the configured request. */
|
|
57
62
|
private appliedThinking: ThinkingLevel | undefined;
|
|
58
63
|
/** Prevent mode-owned thinking changes from being mistaken for manual overrides. */
|
|
59
64
|
private applyingThinking = false;
|
|
@@ -95,36 +100,45 @@ export class UltracodeMode {
|
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
/** Enable if off, disable
|
|
103
|
+
/** Enable auto if off, otherwise disable. Returns the new enabled state. */
|
|
99
104
|
toggle(pi: ExtensionAPI): boolean {
|
|
100
|
-
if (this.
|
|
105
|
+
if (this.isEnabled()) {
|
|
101
106
|
this.disable(pi);
|
|
102
107
|
return false;
|
|
103
108
|
}
|
|
104
|
-
this.enable(pi);
|
|
109
|
+
this.enable(pi, "auto");
|
|
105
110
|
return true;
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
|
|
113
|
+
getMode(): UltracodeModeName {
|
|
114
|
+
return this.mode;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The thinking level Pi actually applied after model/runtime clamping. */
|
|
109
118
|
getAppliedThinking(): ThinkingLevel | undefined {
|
|
110
119
|
return this.appliedThinking;
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
/**
|
|
114
|
-
* Return the raw
|
|
115
|
-
*
|
|
123
|
+
* Return the configured raw effort request so every workflow subagent is
|
|
124
|
+
* clamped independently against its own model.
|
|
116
125
|
*/
|
|
117
126
|
getSubagentThinkingLevel(): ThinkingLevel | undefined {
|
|
118
|
-
return this.isEnforcing() ?
|
|
127
|
+
return this.isEnforcing() ? thinkingLevelForMode(this.mode) : undefined;
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
/** Reassert the
|
|
122
|
-
|
|
130
|
+
/** Reassert the configured mode effort before a turn or after a model change. */
|
|
131
|
+
reapplyConfiguredThinking(pi: ExtensionAPI): boolean {
|
|
123
132
|
if (!this.isEnforcing()) return false;
|
|
124
|
-
this.
|
|
133
|
+
this.applyConfiguredThinking(pi);
|
|
125
134
|
return true;
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
/** @deprecated Use reapplyConfiguredThinking(). */
|
|
138
|
+
reapplyMaximumThinking(pi: ExtensionAPI): boolean {
|
|
139
|
+
return this.reapplyConfiguredThinking(pi);
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
/**
|
|
129
143
|
* Handle model switches both while active and after a clamped restoration.
|
|
130
144
|
* Returns true when Ultracode remains active and the UI should be refreshed.
|
|
@@ -132,8 +146,8 @@ export class UltracodeMode {
|
|
|
132
146
|
handleModelSelect(pi: ExtensionAPI): boolean {
|
|
133
147
|
if (this.suspended) return false;
|
|
134
148
|
this.pendingClearGeneration++;
|
|
135
|
-
if (this.
|
|
136
|
-
this.
|
|
149
|
+
if (this.isEnabled()) {
|
|
150
|
+
this.applyConfiguredThinking(pi);
|
|
137
151
|
return true;
|
|
138
152
|
}
|
|
139
153
|
if (this.pendingPreviousThinking) {
|
|
@@ -147,7 +161,7 @@ export class UltracodeMode {
|
|
|
147
161
|
|
|
148
162
|
/** Restore the pre-mode effective effort without changing persisted mode state. */
|
|
149
163
|
restorePreviousThinking(pi: ExtensionAPI): void {
|
|
150
|
-
if (this.
|
|
164
|
+
if (this.isEnabled() && this.previousThinking) this.applyCompatibleThinking(pi, this.previousThinking);
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
/** Stop enforcing synchronously, then restore effort before session teardown. */
|
|
@@ -162,34 +176,32 @@ export class UltracodeMode {
|
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
/**
|
|
165
|
-
* Enforce the
|
|
166
|
-
* and events emitted by this mode are ignored to avoid
|
|
167
|
-
* Returns true when a manual selection was overridden.
|
|
179
|
+
* Enforce the configured effort after an external thinking-level selection.
|
|
180
|
+
* Stale events and events emitted by this mode are ignored to avoid recursion.
|
|
168
181
|
*/
|
|
169
182
|
handleThinkingLevelSelect(pi: ExtensionAPI, level: ThinkingLevel): boolean {
|
|
170
183
|
if (this.suspended || this.applyingThinking) return false;
|
|
171
184
|
const current = safeGetThinking(pi);
|
|
172
185
|
if (!current || current !== level) return false;
|
|
173
|
-
if (!this.
|
|
186
|
+
if (!this.isEnabled()) {
|
|
174
187
|
// Pi emits the same event for a user selection and an automatic model
|
|
175
188
|
// re-clamp. Defer clearing until model_select has had a chance to consume it.
|
|
176
189
|
if (this.pendingPreviousThinking) this.deferPendingClear(pi, level);
|
|
177
190
|
return false;
|
|
178
191
|
}
|
|
179
192
|
if (current === this.appliedThinking) return false;
|
|
180
|
-
this.
|
|
193
|
+
this.applyConfiguredThinking(pi);
|
|
181
194
|
return true;
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
isEnabled(): boolean {
|
|
185
|
-
return this.
|
|
198
|
+
return this.mode !== "off";
|
|
186
199
|
}
|
|
187
200
|
|
|
188
201
|
isSuspended(): boolean {
|
|
189
202
|
return this.suspended;
|
|
190
203
|
}
|
|
191
204
|
|
|
192
|
-
|
|
193
205
|
/** Keep tool availability aligned with the current mode state. */
|
|
194
206
|
syncWorkflowTool(pi: ExtensionAPI): void {
|
|
195
207
|
if (this.isEnforcing()) this.activateWorkflowTool(pi);
|
|
@@ -200,12 +212,16 @@ export class UltracodeMode {
|
|
|
200
212
|
return ULTRACODE_TAGLINE;
|
|
201
213
|
}
|
|
202
214
|
|
|
203
|
-
/**
|
|
204
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Enable or switch modes without replacing the saved baseline. The no-argument
|
|
217
|
+
* form retains the pre-0.5 programmatic deep behavior; user commands pass an
|
|
218
|
+
* explicit mode and bare `/ultracode` uses toggle() to enter auto.
|
|
219
|
+
*/
|
|
220
|
+
enable(pi: ExtensionAPI, mode: ActiveUltracodeMode = "deep"): void {
|
|
205
221
|
this.suspended = false;
|
|
206
222
|
this.pendingPreviousThinking = undefined;
|
|
207
223
|
this.pendingClearGeneration++;
|
|
208
|
-
if (!this.
|
|
224
|
+
if (!this.isEnabled()) {
|
|
209
225
|
const current = safeGetThinking(pi);
|
|
210
226
|
const preference = this.captureThinkingPreference();
|
|
211
227
|
const effectivePreference = this.runtimeCompatibleThinking(preference.effective) as
|
|
@@ -222,16 +238,16 @@ export class UltracodeMode {
|
|
|
222
238
|
: current;
|
|
223
239
|
this.previousDefaultThinking = this.runtimeCompatibleThinking(preference.global);
|
|
224
240
|
this.legacyDefaultMigrationPending = false;
|
|
225
|
-
this.enabled = true;
|
|
226
241
|
}
|
|
227
|
-
this.
|
|
242
|
+
this.mode = mode;
|
|
243
|
+
this.applyConfiguredThinking(pi);
|
|
228
244
|
this.syncWorkflowTool(pi);
|
|
229
245
|
this.persist(pi);
|
|
230
246
|
}
|
|
231
247
|
|
|
232
|
-
/** Turn
|
|
248
|
+
/** Turn Ultracode off, restoring the pre-mode thinking level. */
|
|
233
249
|
disable(pi: ExtensionAPI): void {
|
|
234
|
-
if (!this.
|
|
250
|
+
if (!this.isEnabled()) {
|
|
235
251
|
this.syncWorkflowTool(pi);
|
|
236
252
|
return;
|
|
237
253
|
}
|
|
@@ -241,7 +257,7 @@ export class UltracodeMode {
|
|
|
241
257
|
? previous
|
|
242
258
|
: undefined;
|
|
243
259
|
this.pendingClearGeneration++;
|
|
244
|
-
this.
|
|
260
|
+
this.mode = "off";
|
|
245
261
|
this.suspended = false;
|
|
246
262
|
this.syncWorkflowTool(pi);
|
|
247
263
|
this.persist(pi);
|
|
@@ -286,7 +302,7 @@ export class UltracodeMode {
|
|
|
286
302
|
?? (wasEnforcing ? this.previousThinking : undefined)
|
|
287
303
|
?? effectivePreference,
|
|
288
304
|
) as ThinkingLevel | undefined;
|
|
289
|
-
this.
|
|
305
|
+
this.mode = "off";
|
|
290
306
|
this.suspended = false;
|
|
291
307
|
this.pendingPreviousThinking = undefined;
|
|
292
308
|
this.previousThinking = target;
|
|
@@ -306,7 +322,7 @@ export class UltracodeMode {
|
|
|
306
322
|
}
|
|
307
323
|
|
|
308
324
|
this.suspended = false;
|
|
309
|
-
this.
|
|
325
|
+
this.mode = latest.mode;
|
|
310
326
|
this.syncWorkflowTool(pi);
|
|
311
327
|
const maxIsUnknownToRuntime = !this.runtimeSupportsMaxThinking
|
|
312
328
|
&& preference.effective === ULTRACODE_THINKING_LEVEL;
|
|
@@ -319,7 +335,7 @@ export class UltracodeMode {
|
|
|
319
335
|
// Pre-preference-store releases persisted only previousThinking while their
|
|
320
336
|
// active xhigh request polluted Pi's global default. Recover that baseline
|
|
321
337
|
// once instead of treating the known Ultracode value as a user preference.
|
|
322
|
-
const migratesLegacyDefault = latest.
|
|
338
|
+
const migratesLegacyDefault = latest.mode !== "off"
|
|
323
339
|
&& latest.previousDefaultThinking === undefined
|
|
324
340
|
&& latest.previousThinking !== undefined
|
|
325
341
|
&& (preference.global === LEGACY_ULTRACODE_THINKING_LEVEL
|
|
@@ -331,9 +347,9 @@ export class UltracodeMode {
|
|
|
331
347
|
: this.runtimeCompatibleThinking(latest.previousDefaultThinking);
|
|
332
348
|
this.legacyDefaultMigrationPending = migratesLegacyDefault;
|
|
333
349
|
|
|
334
|
-
this.pendingPreviousThinking = this.
|
|
335
|
-
if (this.
|
|
336
|
-
this.
|
|
350
|
+
this.pendingPreviousThinking = this.isEnabled() ? undefined : latest.pendingPreviousThinking;
|
|
351
|
+
if (this.isEnabled()) {
|
|
352
|
+
this.applyConfiguredThinking(pi);
|
|
337
353
|
if (migratesLegacyDefault) {
|
|
338
354
|
this.queueDefaultThinkingRestore(() => {
|
|
339
355
|
this.legacyDefaultMigrationPending = false;
|
|
@@ -374,30 +390,29 @@ export class UltracodeMode {
|
|
|
374
390
|
}
|
|
375
391
|
}
|
|
376
392
|
|
|
377
|
-
/**
|
|
378
|
-
* Build the before_agent_start result: appends the ultracode system block to the
|
|
379
|
-
* turn's system prompt when enabled.
|
|
380
|
-
*/
|
|
393
|
+
/** Append the configured semantic-depth policy to the turn's system prompt. */
|
|
381
394
|
beforeAgentStart(event: { systemPrompt: string }): { systemPrompt: string } | undefined {
|
|
382
|
-
if (!this.isEnforcing()) return undefined;
|
|
383
|
-
const block = ultracodeSystemBlock();
|
|
395
|
+
if (!this.isEnforcing() || !isActiveUltracodeMode(this.mode)) return undefined;
|
|
396
|
+
const block = ultracodeSystemBlock(this.mode);
|
|
384
397
|
return { systemPrompt: `${event.systemPrompt}\n\n${block}\n\n${ULTRACODE_ACTIVE_REMINDER}` };
|
|
385
398
|
}
|
|
386
399
|
|
|
387
400
|
statusLine(): string {
|
|
388
|
-
if (!this.
|
|
389
|
-
const parts = [
|
|
401
|
+
if (!this.isEnabled()) return "ultracode: off";
|
|
402
|
+
const parts = [`ultracode: ${this.mode}`];
|
|
390
403
|
// Show the level that actually applied, including compatibility/model fallback.
|
|
391
404
|
if (this.appliedThinking) parts.push(this.appliedThinking);
|
|
392
405
|
return parts.join(" · ");
|
|
393
406
|
}
|
|
394
407
|
|
|
395
|
-
private
|
|
408
|
+
private applyConfiguredThinking(pi: ExtensionAPI): void {
|
|
409
|
+
const target = thinkingLevelForMode(this.mode);
|
|
410
|
+
if (!target) return;
|
|
396
411
|
const writeGeneration = this.preferenceWriteGeneration;
|
|
397
|
-
this.applyCompatibleThinking(pi,
|
|
412
|
+
this.applyCompatibleThinking(pi, target);
|
|
398
413
|
// Pi normally skips persistence when the effective level is unchanged, but
|
|
399
414
|
// the extension API does not promise that. Defensively restore the raw
|
|
400
|
-
// baseline even after a stable
|
|
415
|
+
// baseline even after a stable mode-owned request.
|
|
401
416
|
if (writeGeneration === this.preferenceWriteGeneration) {
|
|
402
417
|
this.queueDefaultThinkingRestore();
|
|
403
418
|
}
|
|
@@ -433,7 +448,7 @@ export class UltracodeMode {
|
|
|
433
448
|
}
|
|
434
449
|
|
|
435
450
|
isEnforcing(): boolean {
|
|
436
|
-
return this.
|
|
451
|
+
return this.isEnabled() && !this.suspended;
|
|
437
452
|
}
|
|
438
453
|
|
|
439
454
|
private pendingRestoreSucceeded(pending: ThinkingLevel): boolean {
|
|
@@ -495,7 +510,7 @@ export class UltracodeMode {
|
|
|
495
510
|
setImmediate(() => {
|
|
496
511
|
if (
|
|
497
512
|
generation !== this.pendingClearGeneration
|
|
498
|
-
|| this.
|
|
513
|
+
|| this.isEnabled()
|
|
499
514
|
|| this.suspended
|
|
500
515
|
|| safeGetThinking(pi) !== level
|
|
501
516
|
) return;
|
|
@@ -528,7 +543,7 @@ export class UltracodeMode {
|
|
|
528
543
|
|
|
529
544
|
private persist(pi: ExtensionAPI): void {
|
|
530
545
|
const state: PersistedModeState = {
|
|
531
|
-
|
|
546
|
+
mode: this.mode,
|
|
532
547
|
previousThinking: this.previousThinking,
|
|
533
548
|
previousDefaultThinking: this.legacyDefaultMigrationPending
|
|
534
549
|
? undefined
|
|
@@ -547,8 +562,17 @@ function parsePersistedModeState(data: unknown): PersistedModeState | undefined
|
|
|
547
562
|
if (!data || typeof data !== "object") return undefined;
|
|
548
563
|
const value = data as Record<string, unknown>;
|
|
549
564
|
const previousDefault = value.previousDefaultThinking;
|
|
565
|
+
// Sessions written before semantic-depth modes stored only enabled:boolean.
|
|
566
|
+
// Preserve their behavior by migrating enabled:true to the old deep mode.
|
|
567
|
+
const mode: UltracodeModeName = isActiveUltracodeMode(value.mode)
|
|
568
|
+
? value.mode
|
|
569
|
+
: value.mode === "off"
|
|
570
|
+
? "off"
|
|
571
|
+
: value.enabled === true
|
|
572
|
+
? "deep"
|
|
573
|
+
: "off";
|
|
550
574
|
return {
|
|
551
|
-
|
|
575
|
+
mode,
|
|
552
576
|
previousThinking: isThinkingLevel(value.previousThinking) ? value.previousThinking : undefined,
|
|
553
577
|
previousDefaultThinking: previousDefault === null || isThinkingLevel(previousDefault)
|
|
554
578
|
? previousDefault
|
package/src/prompts.ts
CHANGED
|
@@ -1,59 +1,92 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* These strings reproduce the behavioural contract of Claude Code's "ultracode"
|
|
5
|
-
* effort level: a standing opt-in to deterministic multi-agent workflow
|
|
6
|
-
* orchestration, biased toward the most exhaustive, correct answer.
|
|
7
|
-
*/
|
|
1
|
+
/** Prompt text for Ultracode's adaptive analysis-depth modes. */
|
|
2
|
+
|
|
3
|
+
import type { ActiveUltracodeMode } from "./depth.ts";
|
|
8
4
|
|
|
9
5
|
/** One-line description shown by `/ultracode status` and the footer. */
|
|
10
|
-
export const ULTRACODE_TAGLINE = "
|
|
6
|
+
export const ULTRACODE_TAGLINE = "semantic-depth workflow orchestration";
|
|
11
7
|
|
|
12
8
|
/**
|
|
13
|
-
* The standing system-prompt block injected on every turn while
|
|
14
|
-
*
|
|
9
|
+
* The standing system-prompt block injected on every turn while Ultracode is
|
|
10
|
+
* active. The parent model performs the semantic routing itself; starting a
|
|
11
|
+
* separate agent merely to classify depth would defeat the focused path.
|
|
15
12
|
*/
|
|
16
|
-
export function ultracodeSystemBlock(): string {
|
|
13
|
+
export function ultracodeSystemBlock(mode: ActiveUltracodeMode = "deep"): string {
|
|
17
14
|
return [
|
|
18
15
|
"<ultracode>",
|
|
19
|
-
|
|
16
|
+
`Configured mode: ${mode}.`,
|
|
17
|
+
"Analysis depth is a semantic quality decision, never a wall-clock decision. Do not use elapsed time, deadlines, or duration limits to choose, lower, or stop analysis depth.",
|
|
18
|
+
"Use the smallest depth that can establish a correct answer. Depth is controlled by research rounds, independent perspectives, verification strength, evidence requirements, skeptic count, and per-agent reasoning effort.",
|
|
19
|
+
"Existing maxAgents/reserveAgents limits are structural admission bounds, not evidence that every available slot should be used.",
|
|
20
20
|
"",
|
|
21
|
-
|
|
22
|
-
"For multi-phase work (understand → design → implement → review), prefer one bounded workflow and inspect the result before deciding whether another workflow is justified. Do not run consecutive workflows by default; continue only when new evidence changes the plan.",
|
|
21
|
+
...modeInstructions(mode),
|
|
23
22
|
"",
|
|
24
|
-
"
|
|
23
|
+
"Evidence-driven escalation and stopping:",
|
|
24
|
+
"- Escalate only for material correctness risk, missing direct evidence, conflicting findings, or an unresolved question that can change the answer.",
|
|
25
|
+
"- Treat security, GC, ABI, deoptimization, concurrency, data-loss, irreversible operations, and critical architecture semantics as high-risk unless bounded evidence proves otherwise.",
|
|
26
|
+
"- Stop when key claims have direct evidence, no material conflict or unresolved high-risk question remains, and another round would only repeat known evidence.",
|
|
27
|
+
"- Model-reported confidence alone is not sufficient. Prefer concrete citations, reproduction, tests, and independent agreement.",
|
|
28
|
+
"- If fixed focused/standard is insufficient, report the remaining uncertainty and recommend a deeper mode; in deep, report any irreducible uncertainty. Never silently exceed a fixed mode.",
|
|
25
29
|
"",
|
|
26
|
-
"
|
|
27
|
-
"-
|
|
28
|
-
"-
|
|
29
|
-
"-
|
|
30
|
-
"-
|
|
31
|
-
"-
|
|
32
|
-
"- No silent caps: if a workflow bounds coverage (top-N, no-retry, sampling), log() what was dropped.",
|
|
33
|
-
"",
|
|
34
|
-
"Scale to the task: \"find any bugs\" → a few finders, single-vote verify; \"thoroughly audit\" / \"be comprehensive\" → a larger but explicit finder pool, 3–5 vote adversarial pass, and a synthesis stage within maxAgents.",
|
|
30
|
+
"Workflow policy:",
|
|
31
|
+
"- Use a workflow only when independent decomposition, verification, isolation, or context scale provides real value. Otherwise solve directly in the parent session.",
|
|
32
|
+
"- Make skeptics conditional: verify high-risk, conflicting, weakly evidenced, or low-confidence claims instead of automatically verifying every branch.",
|
|
33
|
+
"- Avoid a separate synthesis agent when deterministic merging or parent synthesis is enough. Use an adjudicator only when a material conflict remains.",
|
|
34
|
+
"- Match effort to the stage with per-call model suffixes when useful: focused discovery/synthesis may use :medium, standard analysis :high, and :max is reserved for deep or decisive high-risk verification.",
|
|
35
|
+
"- When a workflow runs, log `analysis-depth: <level> — <reason>` before launching agents, `analysis-escalation: ...` for each semantic escalation, and `analysis-stop: ...` for the final evidence-based stop reason. Never use time as an escalation or stop reason.",
|
|
35
36
|
"</ultracode>",
|
|
36
37
|
].join("\n");
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
function modeInstructions(mode: ActiveUltracodeMode): string[] {
|
|
41
|
+
switch (mode) {
|
|
42
|
+
case "auto":
|
|
43
|
+
return [
|
|
44
|
+
"Before acting, silently route this task to focused, standard, or deep. Do not spawn a router agent.",
|
|
45
|
+
"Choose from user intent, consequence risk, scope, ambiguity, available evidence, cross-module or cross-repository breadth, and whether independent verification is necessary.",
|
|
46
|
+
"Begin at the shallowest sufficient depth and escalate only when evidence triggers an escalation condition below.",
|
|
47
|
+
];
|
|
48
|
+
case "focused":
|
|
49
|
+
return [
|
|
50
|
+
"Focused is a fixed lightweight depth: prefer one bounded line of inquiry and the normal parent-agent loop.",
|
|
51
|
+
"Do not run adversarial verification by default. Use at most a small, narrowly scoped delegation only when it clearly reduces duplicated exploration.",
|
|
52
|
+
];
|
|
53
|
+
case "standard":
|
|
54
|
+
return [
|
|
55
|
+
"Standard is a fixed balanced depth: cover the few independent dimensions that can materially change the answer.",
|
|
56
|
+
"Prefer one discovery round and conditional verification of only disputed, high-risk, or weakly evidenced claims.",
|
|
57
|
+
];
|
|
58
|
+
case "deep":
|
|
59
|
+
return [
|
|
60
|
+
"Deep is a fixed high-assurance depth: default to a bounded workflow for substantive tasks unless the work is conversational, trivial, or already verified.",
|
|
61
|
+
"Use multi-perspective investigation and adversarial verification where the task supports it. Choose fan-out and round limits before launch.",
|
|
62
|
+
"Use at most two discovery rounds by default; add a third only when the user explicitly requests exhaustive coverage and fresh evidence is still appearing.",
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Short reminder appended after the standing block. */
|
|
40
68
|
export const ULTRACODE_ACTIVE_REMINDER =
|
|
41
|
-
"Reminder:
|
|
69
|
+
"Reminder: Ultracode is active — obey its configured semantic depth, escalate only from evidence, and never use elapsed time as an analysis budget.";
|
|
42
70
|
|
|
43
71
|
/** Tool description for the `workflow` tool. */
|
|
44
72
|
export const WORKFLOW_TOOL_DESCRIPTION = [
|
|
45
|
-
"Execute a deterministic JavaScript workflow that orchestrates
|
|
73
|
+
"Execute a deterministic JavaScript workflow that orchestrates a proportionate set of subagents for independent coverage, targeted verification, or work that exceeds one context.",
|
|
74
|
+
"Choose the smallest workflow justified by the configured Ultracode depth and current evidence; workflow use and adversarial checks are not automatic.",
|
|
46
75
|
"Each invocation must provide workflow source via `script`, `scriptPath`, or `name`. Inline `script` must be raw JavaScript (no Markdown fences) beginning with `export const meta = { name, description }` (a pure literal) and should call agent() at least once for useful orchestration.",
|
|
47
|
-
"Available globals: agent(prompt, opts), parallel(thunks, options?), pipeline(items, ...stages), phase(title), log(message), workflow(
|
|
76
|
+
"Available globals: agent(prompt, opts), parallel(thunks, options?), pipeline(items, ...stages), phase(title), log(message), workflow(name, args), args, cwd. The tool accepts maxAgents (default 128, max 1024) as a lifetime live-agent admission cap across resumes, not a token or depth budget; cache replay is free.",
|
|
48
77
|
].join(" ");
|
|
49
78
|
|
|
50
79
|
/** One-line snippet for the Available tools section. */
|
|
51
80
|
export const WORKFLOW_PROMPT_SNIPPET =
|
|
52
|
-
"Run a deterministic JS workflow
|
|
81
|
+
"Run a deterministic JS workflow with proportionate subagent fan-out. Header: export const meta = { name: 'snake_case', description: '...' }. Globals: agent/parallel/pipeline/phase/log/workflow/args/cwd.";
|
|
53
82
|
|
|
54
83
|
/** Guideline bullets appended to the Guidelines section when the tool is active. */
|
|
55
84
|
export const WORKFLOW_GUIDELINES: string[] = [
|
|
56
|
-
"
|
|
85
|
+
"Use the workflow tool only when independent decomposition, targeted verification, isolation, or context scale adds value; use the parent agent directly for a bounded task.",
|
|
86
|
+
"For an auto-depth workflow, log `analysis-depth: <level> — <reason>` before launching agents, `analysis-escalation: ...` when evidence requires more depth, and `analysis-stop: ...` when evidence is sufficient. Elapsed time must never determine depth.",
|
|
87
|
+
"For workflow verification, run skeptics only for high-risk, conflicting, low-confidence, or weakly evidenced claims. Do not automatically attach a skeptic to every branch.",
|
|
88
|
+
"For workflow synthesis, prefer structured results plus deterministic or parent-session merging. Start a synthesis/adjudication agent only for a material unresolved conflict, and normally give pure synthesis lower effort and no broad source-search mandate.",
|
|
89
|
+
"For workflow agent effort, use a model suffix such as `:medium`, `:high`, or `:max` when the stage should differ from the mode default; reserve max for deep investigation or decisive high-risk verification.",
|
|
57
90
|
"For the workflow tool, provide workflow source with `script`, `scriptPath`, or `name`. Inline `script` must be one raw JavaScript string: no Markdown fences, no prose around the script.",
|
|
58
91
|
"For the workflow tool, the script's first statement must be `export const meta = { name: 'short_snake_case', description: 'non-empty human description' }`. meta must be a pure literal: no variables, function calls, spreads, or template interpolation. meta.phases is optional and should mirror your phase() titles.",
|
|
59
92
|
"For the workflow tool, write plain JavaScript after the meta export. No TypeScript syntax, imports, require(), fs, network, Date/Intl/Temporal, Math.random(), binary memory constructors, WebAssembly, or dynamic method calls such as value[key](...) (they bypass deterministic checks or resource bounds). Stamp timestamps after the workflow returns; vary randomness by agent index.",
|
|
@@ -64,9 +97,9 @@ export const WORKFLOW_GUIDELINES: string[] = [
|
|
|
64
97
|
"For the workflow tool, if agent() needs machine-readable output pass an inline JSON Schema via opts.schema; agent() then returns the validated object. Use only the bounded subset: types, object/array structure, enum/const, anyOf/allOf, length/numeric constraints, and annotations. Unknown keywords, $ref variants, oneOf, format, pattern, and patternProperties are rejected. Keep schemas under 256 KiB / 64 levels and every agent/workflow output under 2 MiB. Use JSON Schema, not TypeScript or TypeBox constructors.",
|
|
65
98
|
"For the workflow tool, when agent() is called WITHOUT a schema, its return value is the subagent's final assistant text (the last text the subagent produced). With a schema it returns the validated structured_output object. Prefer a schema for machine-readable results; use the text form only for prose summaries.",
|
|
66
99
|
"For the workflow tool, agent opts also accept: model (override the subagent model by pattern), agentType (use a custom subagent role/system-prompt), isolation:'worktree' (run the agent in an isolated git worktree — use ONLY when agents mutate files in parallel and would conflict), and phase (assign the agent to a progress group explicitly inside parallel()/pipeline()).",
|
|
67
|
-
"For the workflow tool, workflow(
|
|
100
|
+
"For the workflow tool, workflow(name, args) runs a trust-aware saved workflow by name as a sub-step, sharing this run's concurrency, maxAgents, active reservations, and agent counter. Explicit nested script paths are not supported. Nesting is one level only. Child subagent sessions do not load ambient extensions or expose workflow/subagent orchestration tools, so they cannot start an independent recursive workflow chain.",
|
|
68
101
|
"For workflow resumeFromRunId, reuse the exact same script and args. Resume is immutable, successful calls replay by stable structural call path, and maxAgents may only stay the same or increase; changed work must start a new run.",
|
|
69
|
-
"For the workflow tool, failed agent()/parallel()/pipeline() branches return null and log the failure (unless the whole run is aborted or a policy limit is hit). Check for nulls before synthesizing conclusions, and prefer a final
|
|
102
|
+
"For the workflow tool, failed agent()/parallel()/pipeline() branches return null and log the failure (unless the whole run is aborted or a policy limit is hit). Check for nulls before synthesizing conclusions, and prefer a final compact JSON-serializable result.",
|
|
70
103
|
"For the workflow tool, directly await or return every orchestration promise (agent/parallel/pipeline/workflow). Native .then/.catch/.finally chains and Promise.all/allSettled/race/any are rejected; use parallel() or pipeline() so call identity stays deterministic. Unobserved, pending, or native same-scope concurrent orchestration is fatal.",
|
|
71
104
|
"Workflow helpers that call orchestration must be directly declared functions/function variables or static methods on a declared object/stored class instance. Do not alias them, assign them later, forward through this.otherMethod(), or call them from temporary/awaited factory receivers; rewrite those forms as a direct declared helper so resume identity remains stable.",
|
|
72
105
|
"For the workflow tool, do not assume subagents share the parent's repository context; include enough task context and relevant file paths in each agent prompt.",
|
package/src/thinking.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhig
|
|
|
4
4
|
|
|
5
5
|
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** Deep mode asks Pi for the strongest effort the selected model supports. */
|
|
8
8
|
export const ULTRACODE_THINKING_LEVEL: ThinkingLevel = "max";
|
|
9
9
|
|
|
10
10
|
/** Compatibility retry for Pi versions released before the `max` level existed. */
|
|
@@ -52,6 +52,7 @@ const PARENT_ONLY_CHILD_SKILLS = new Set(["pi-subagents"]);
|
|
|
52
52
|
export interface WorkflowChildResourceLoaderOptions {
|
|
53
53
|
cwd: string;
|
|
54
54
|
agentDir: string;
|
|
55
|
+
projectTrusted?: boolean;
|
|
55
56
|
settingsManager?: SettingsManager;
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -67,7 +68,9 @@ export async function createWorkflowChildResourceLoader(
|
|
|
67
68
|
options: WorkflowChildResourceLoaderOptions,
|
|
68
69
|
): Promise<DefaultResourceLoader> {
|
|
69
70
|
const settingsManager = options.settingsManager
|
|
70
|
-
?? SettingsManager.create(options.cwd, options.agentDir
|
|
71
|
+
?? SettingsManager.create(options.cwd, options.agentDir, {
|
|
72
|
+
projectTrusted: options.projectTrusted ?? false,
|
|
73
|
+
});
|
|
71
74
|
const loader = new DefaultResourceLoader({
|
|
72
75
|
cwd: options.cwd,
|
|
73
76
|
agentDir: options.agentDir,
|
|
@@ -221,6 +224,8 @@ export type AgentSessionFactory = (
|
|
|
221
224
|
|
|
222
225
|
export interface WorkflowAgentRunnerOptions {
|
|
223
226
|
cwd: string;
|
|
227
|
+
/** Parent session's immutable project-trust decision. */
|
|
228
|
+
projectTrusted?: boolean;
|
|
224
229
|
/** Synchronous extension facade used only for model selection and state replay. */
|
|
225
230
|
modelRegistry?: ModelRegistryLike;
|
|
226
231
|
/** Canonical runtime to share across child sessions when supplied by an SDK host. */
|
|
@@ -278,6 +283,7 @@ export interface AgentRunCall {
|
|
|
278
283
|
|
|
279
284
|
export class WorkflowAgentRunner {
|
|
280
285
|
private readonly baseCwd: string;
|
|
286
|
+
private readonly projectTrusted: boolean;
|
|
281
287
|
private readonly modelRegistry?: ModelRegistryLike;
|
|
282
288
|
private readonly providedModelRuntime?: ModelRuntimeLike;
|
|
283
289
|
private readonly defaultModel?: ModelLike;
|
|
@@ -290,6 +296,7 @@ export class WorkflowAgentRunner {
|
|
|
290
296
|
|
|
291
297
|
constructor(options: WorkflowAgentRunnerOptions) {
|
|
292
298
|
this.baseCwd = options.cwd;
|
|
299
|
+
this.projectTrusted = options.projectTrusted ?? false;
|
|
293
300
|
this.modelRegistry = options.modelRegistry;
|
|
294
301
|
this.providedModelRuntime = options.modelRuntime;
|
|
295
302
|
this.defaultModel = options.model;
|
|
@@ -304,10 +311,11 @@ export class WorkflowAgentRunner {
|
|
|
304
311
|
async run(call: AgentRunCall): Promise<AgentRunResult> {
|
|
305
312
|
if (call.signal?.aborted) throw new Error("Subagent was aborted");
|
|
306
313
|
|
|
307
|
-
const
|
|
314
|
+
const executionCwd = call.cwd ?? this.baseCwd;
|
|
315
|
+
const resourceCwd = this.baseCwd;
|
|
308
316
|
const capture: StructuredOutputCapture<any> = { called: false, value: undefined };
|
|
309
317
|
|
|
310
|
-
const customTools: ToolDefinition[] = [...createCodingTools(
|
|
318
|
+
const customTools: ToolDefinition[] = [...createCodingTools(executionCwd)];
|
|
311
319
|
let toolAllowlist: string[] | undefined = call.agentTypeDef?.tools
|
|
312
320
|
? [...call.agentTypeDef.tools]
|
|
313
321
|
: undefined;
|
|
@@ -346,14 +354,21 @@ export class WorkflowAgentRunner {
|
|
|
346
354
|
}
|
|
347
355
|
|
|
348
356
|
const createSession = async (level: ThinkingLevel | undefined) => {
|
|
349
|
-
const settingsManager = SettingsManager.create(
|
|
357
|
+
const settingsManager = SettingsManager.create(resourceCwd, agentDir, {
|
|
358
|
+
projectTrusted: this.projectTrusted,
|
|
359
|
+
});
|
|
350
360
|
const resourceLoader = this.usesPiSessionFactory
|
|
351
|
-
? await createWorkflowChildResourceLoader({
|
|
361
|
+
? await createWorkflowChildResourceLoader({
|
|
362
|
+
cwd: resourceCwd,
|
|
363
|
+
agentDir,
|
|
364
|
+
projectTrusted: this.projectTrusted,
|
|
365
|
+
settingsManager,
|
|
366
|
+
})
|
|
352
367
|
: undefined;
|
|
353
368
|
return this.createSession({
|
|
354
|
-
cwd,
|
|
369
|
+
cwd: executionCwd,
|
|
355
370
|
agentDir,
|
|
356
|
-
sessionManager: SessionManager.inMemory(
|
|
371
|
+
sessionManager: SessionManager.inMemory(executionCwd),
|
|
357
372
|
settingsManager,
|
|
358
373
|
...(resourceLoader ? { resourceLoader } : {}),
|
|
359
374
|
customTools,
|
|
@@ -484,7 +499,7 @@ export class WorkflowAgentRunner {
|
|
|
484
499
|
usage: readUsage(session, telemetryCounters),
|
|
485
500
|
modelId: actualModelId,
|
|
486
501
|
effort: actualEffort,
|
|
487
|
-
cwd,
|
|
502
|
+
cwd: executionCwd,
|
|
488
503
|
};
|
|
489
504
|
} catch (error) {
|
|
490
505
|
hasPrimaryError = true;
|