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/src/mode.ts CHANGED
@@ -1,16 +1,21 @@
1
1
  /**
2
2
  * Ultracode mode controller.
3
3
  *
4
- * Ultracode is a session-scoped effort mode. While on, it:
5
- * - raises the thinking level to the model's maximum (remembering the previous level),
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 a standing "author and run a workflow by default" system block on
8
- * every turn,
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
- enabled: boolean;
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 enabled = false;
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 maximum request. */
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 if on. Returns the new enabled state. */
103
+ /** Enable auto if off, otherwise disable. Returns the new enabled state. */
99
104
  toggle(pi: ExtensionAPI): boolean {
100
- if (this.enabled) {
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
- /** The thinking level Pi actually applied (`max`, or the model/runtime fallback). */
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 maximum request, not the parent's applied value, so every
115
- * workflow subagent is clamped independently against its own model.
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() ? ULTRACODE_THINKING_LEVEL : undefined;
127
+ return this.isEnforcing() ? thinkingLevelForMode(this.mode) : undefined;
119
128
  }
120
129
 
121
- /** Reassert the maximum before a turn or after a model change. */
122
- reapplyMaximumThinking(pi: ExtensionAPI): boolean {
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.applyUltracodeThinking(pi);
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.enabled) {
136
- this.applyUltracodeThinking(pi);
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.enabled && this.previousThinking) this.applyCompatibleThinking(pi, this.previousThinking);
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 mode after an external thinking-level selection. Stale events
166
- * and events emitted by this mode are ignored to avoid recursive updates.
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.enabled) {
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.applyUltracodeThinking(pi);
193
+ this.applyConfiguredThinking(pi);
181
194
  return true;
182
195
  }
183
196
 
184
197
  isEnabled(): boolean {
185
- return this.enabled;
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
- /** Turn ultracode on. Idempotent. */
204
- enable(pi: ExtensionAPI): void {
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.enabled) {
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.applyUltracodeThinking(pi);
242
+ this.mode = mode;
243
+ this.applyConfiguredThinking(pi);
228
244
  this.syncWorkflowTool(pi);
229
245
  this.persist(pi);
230
246
  }
231
247
 
232
- /** Turn ultracode off, restoring the previous thinking level. */
248
+ /** Turn Ultracode off, restoring the pre-mode thinking level. */
233
249
  disable(pi: ExtensionAPI): void {
234
- if (!this.enabled) {
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.enabled = false;
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.enabled = false;
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.enabled = latest.enabled;
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.enabled
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.enabled ? undefined : latest.pendingPreviousThinking;
335
- if (this.enabled) {
336
- this.applyUltracodeThinking(pi);
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.enabled) return `ultracode: off`;
389
- const parts = ["ultracode: on"];
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 applyUltracodeThinking(pi: ExtensionAPI): void {
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, ULTRACODE_THINKING_LEVEL);
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 max -> max request.
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.enabled && !this.suspended;
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.enabled
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
- enabled: this.enabled,
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
- enabled: value.enabled === true,
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
- * Prompt text for ultracode mode.
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 = "max thinking + dynamic workflow orchestration";
6
+ export const ULTRACODE_TAGLINE = "semantic-depth workflow orchestration";
11
7
 
12
8
  /**
13
- * The standing system-prompt block injected on every turn while ultracode is on.
14
- * Mirrors the "Ultracode" section of the Workflow tool contract.
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
- "Ultracode is ON. This opt-in is standing: author and run a workflow for every substantive task by default.",
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
- "The goal is a bounded, exhaustive-enough, correct answer: choose explicit fan-out and round limits before launching work.",
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
- "Lean toward orchestrating with the workflow tool and adversarially verifying your findings, unless the work is trivial or already verified. Solo (no workflow) only on conversational turns or trivial mechanical edits.",
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
- "Quality patterns to compose as the task calls for it:",
27
- "- Adversarial verify: spawn N independent skeptics per finding, each prompted to REFUTE it; kill the finding if a majority refute. Stops plausible-but-wrong findings from surviving.",
28
- "- Perspective-diverse verify: when a finding can fail in more than one way, give each verifier a distinct lens (correctness, security, performance, does-it-reproduce) instead of N identical refuters.",
29
- "- Multi-modal sweep: parallel agents each searching a different way (by-container, by-content, by-entity, by-time); each is blind to what the others surface.",
30
- "- Loop-until-dry: for unknown-size discovery (bugs, edge cases), run at most 2 rounds by default (3 only when the user explicitly asks for comprehensive coverage); stop sooner when no fresh evidence appears.",
31
- "- Completeness critic: a final agent that asks \"what's missing a modality not run, a claim unverified, a source unread?\" What it finds becomes the next round of work.",
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
- /** Short reminder appended to confirm the mode is active (system-reminder style). */
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: ultracode is ondefault to authoring and running a workflow for substantive tasks, and adversarially verify your findings.";
69
+ "Reminder: Ultracode is activeobey 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 multiple subagents to be comprehensive (decompose and cover in parallel), confident (independent perspectives and adversarial checks before committing), or to take on scale one context can't hold (migrations, audits, broad sweeps).",
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(nameOrRef, args), args, cwd. The tool accepts maxAgents (default 128, max 1024) as a lifetime live-agent admission cap across resumes, not a token budget; cache replay is free.",
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 that fans out subagents. Header: export const meta = { name: 'snake_case', description: '...' }. Globals: agent/parallel/pipeline/phase/log/workflow/args/cwd.";
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
- "While Ultracode is enabled, use the workflow tool to decompose-and-cover in parallel, gather independent perspectives that adversarially verify each other, or take on scale one context can't hold.",
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(nameOrRef, args) runs a saved workflow (by name) or a scriptPath inline as a sub-step, sharing this run's concurrency, maxAgents, active reservations, and agent counter. 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.",
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 synthesis/assertion agent that returns a compact JSON-serializable verdict.",
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
- /** Ultracode always asks Pi for the strongest effort the selected model supports. */
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 cwd = call.cwd ?? this.baseCwd;
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(cwd)];
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(cwd, agentDir);
357
+ const settingsManager = SettingsManager.create(resourceCwd, agentDir, {
358
+ projectTrusted: this.projectTrusted,
359
+ });
350
360
  const resourceLoader = this.usesPiSessionFactory
351
- ? await createWorkflowChildResourceLoader({ cwd, agentDir, settingsManager })
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(cwd),
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;