@oh-my-pi/pi-coding-agent 16.1.5 → 16.1.6

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.
@@ -177,7 +177,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
177
177
  * user submits becomes the new loop prompt and resumes iteration.
178
178
  */
179
179
  pauseLoop(): void;
180
- handleLoopCommand(args?: string): Promise<void>;
180
+ handleLoopCommand(args?: string): Promise<string | undefined>;
181
181
  recordLocalSubmission(text: string, imageCount?: number): () => void;
182
182
  withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: {
183
183
  imageCount?: number;
@@ -14,7 +14,20 @@ export type LoopLimitRuntime = {
14
14
  durationMs: number;
15
15
  deadlineMs: number;
16
16
  };
17
- export declare function parseLoopLimitArgs(args: string): LoopLimitConfig | undefined | string;
17
+ export interface ParsedLoopArgs {
18
+ /** Iteration/duration budget, when the user supplied a leading limit token. */
19
+ limit?: LoopLimitConfig;
20
+ /** Inline loop prompt: text after the limit, or the whole argument when no limit was given. */
21
+ prompt?: string;
22
+ }
23
+ /**
24
+ * Parse `/loop` arguments into an optional leading limit plus an optional inline
25
+ * prompt. A token that *looks* like a limit (starts with a digit or sign) but
26
+ * fails to parse is a hard error; anything else is treated as prompt text, so
27
+ * plain prose after `/loop` keeps starting an unbounded loop instead of erroring
28
+ * (the pre-arg-parsing behavior). Returns the error message string on failure.
29
+ */
30
+ export declare function parseLoopLimitArgs(args: string): ParsedLoopArgs | string;
18
31
  export declare function createLoopLimitRuntime(config: LoopLimitConfig | undefined, nowMs?: number): LoopLimitRuntime | undefined;
19
32
  export declare function consumeLoopLimitIteration(limit: LoopLimitRuntime | undefined, nowMs?: number): boolean;
20
33
  export declare function isLoopDurationExpired(limit: LoopLimitRuntime | undefined, nowMs?: number): boolean;
@@ -339,7 +339,7 @@ export interface InteractiveModeContext {
339
339
  handlePlanModeCommand(initialPrompt?: string): Promise<void>;
340
340
  handleGoalModeCommand(rest?: string): Promise<void>;
341
341
  handleGuidedGoalCommand(rest?: string): Promise<void>;
342
- handleLoopCommand(args?: string): Promise<void>;
342
+ handleLoopCommand(args?: string): Promise<string | undefined>;
343
343
  disableLoopMode(): void;
344
344
  pauseLoop(): void;
345
345
  handlePlanApproval(details: PlanApprovalDetails): Promise<void>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.1.5",
4
+ "version": "16.1.6",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -48,17 +48,17 @@
48
48
  "@agentclientprotocol/sdk": "0.25.0",
49
49
  "@babel/parser": "^7.29.7",
50
50
  "@mozilla/readability": "^0.6.0",
51
- "@oh-my-pi/hashline": "16.1.5",
52
- "@oh-my-pi/omp-stats": "16.1.5",
53
- "@oh-my-pi/pi-agent-core": "16.1.5",
54
- "@oh-my-pi/pi-ai": "16.1.5",
55
- "@oh-my-pi/pi-catalog": "16.1.5",
56
- "@oh-my-pi/pi-mnemopi": "16.1.5",
57
- "@oh-my-pi/pi-natives": "16.1.5",
58
- "@oh-my-pi/pi-tui": "16.1.5",
59
- "@oh-my-pi/pi-utils": "16.1.5",
60
- "@oh-my-pi/pi-wire": "16.1.5",
61
- "@oh-my-pi/snapcompact": "16.1.5",
51
+ "@oh-my-pi/hashline": "16.1.6",
52
+ "@oh-my-pi/omp-stats": "16.1.6",
53
+ "@oh-my-pi/pi-agent-core": "16.1.6",
54
+ "@oh-my-pi/pi-ai": "16.1.6",
55
+ "@oh-my-pi/pi-catalog": "16.1.6",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.6",
57
+ "@oh-my-pi/pi-natives": "16.1.6",
58
+ "@oh-my-pi/pi-tui": "16.1.6",
59
+ "@oh-my-pi/pi-utils": "16.1.6",
60
+ "@oh-my-pi/pi-wire": "16.1.6",
61
+ "@oh-my-pi/snapcompact": "16.1.6",
62
62
  "@opentelemetry/api": "^1.9.1",
63
63
  "@opentelemetry/context-async-hooks": "^2.7.1",
64
64
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -1128,27 +1128,33 @@ export class InteractiveMode implements InteractiveModeContext {
1128
1128
  this.#cancelLoopAutoSubmit();
1129
1129
  }
1130
1130
 
1131
- async handleLoopCommand(args = ""): Promise<void> {
1131
+ async handleLoopCommand(args = ""): Promise<string | undefined> {
1132
1132
  if (this.loopModeEnabled) {
1133
1133
  this.disableLoopMode();
1134
- return;
1134
+ return undefined;
1135
1135
  }
1136
- const parsedLimit = parseLoopLimitArgs(args);
1137
- if (typeof parsedLimit === "string") {
1138
- this.showError(parsedLimit);
1139
- return;
1136
+ const parsed = parseLoopLimitArgs(args);
1137
+ if (typeof parsed === "string") {
1138
+ this.showError(parsed);
1139
+ return undefined;
1140
1140
  }
1141
1141
  this.loopModeEnabled = true;
1142
1142
  this.loopPrompt = undefined;
1143
- this.loopLimit = createLoopLimitRuntime(parsedLimit);
1143
+ this.loopLimit = createLoopLimitRuntime(parsed.limit);
1144
1144
  this.statusLine.setLoopModeStatus({ enabled: true });
1145
1145
  this.updateEditorTopBorder();
1146
1146
  this.ui.requestRender();
1147
- const limitSuffix = parsedLimit ? ` Limited to ${describeLoopLimit(parsedLimit)}.` : "";
1147
+ const limitSuffix = parsed.limit ? ` Limited to ${describeLoopLimit(parsed.limit)}.` : "";
1148
1148
  const remainingSuffix = this.loopLimit ? ` ${describeLoopLimitRuntime(this.loopLimit)}.` : "";
1149
+ const tail = parsed.prompt ? "Repeating it after each turn." : "Your next prompt will repeat after each turn.";
1149
1150
  this.showStatus(
1150
- `Loop mode enabled.${limitSuffix}${remainingSuffix} Your next prompt will repeat after each turn. Esc cancels the current iteration; /loop again to disable.`,
1151
+ `Loop mode enabled.${limitSuffix}${remainingSuffix} ${tail} Esc cancels the current iteration; /loop again to disable.`,
1151
1152
  );
1153
+ // Hand any inline prompt back to the dispatcher so the normal submit flow
1154
+ // runs the first iteration — it records the text as the loop prompt and
1155
+ // auto-resubmits it after each yield, identical to typing the prompt right
1156
+ // after enabling loop mode.
1157
+ return parsed.prompt;
1152
1158
  }
1153
1159
 
1154
1160
  recordLocalSubmission(text: string, imageCount = 0): () => void {
@@ -38,53 +38,105 @@ const TIME_UNITS_MS = new Map<string, number>([
38
38
  ["hours", 3_600_000],
39
39
  ]);
40
40
 
41
- export function parseLoopLimitArgs(args: string): LoopLimitConfig | undefined | string {
42
- const trimmed = args.trim().toLowerCase();
43
- if (!trimmed) return undefined;
41
+ const LOOP_USAGE = "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min.";
44
42
 
45
- const parts = trimmed.split(/\s+/);
46
- if (parts.length > 2) {
47
- return "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min.";
48
- }
43
+ export interface ParsedLoopArgs {
44
+ /** Iteration/duration budget, when the user supplied a leading limit token. */
45
+ limit?: LoopLimitConfig;
46
+ /** Inline loop prompt: text after the limit, or the whole argument when no limit was given. */
47
+ prompt?: string;
48
+ }
49
49
 
50
- if (parts.length === 2) {
51
- return parseDurationParts(parts[0], parts[1]);
50
+ /**
51
+ * Parse `/loop` arguments into an optional leading limit plus an optional inline
52
+ * prompt. A token that *looks* like a limit (starts with a digit or sign) but
53
+ * fails to parse is a hard error; anything else is treated as prompt text, so
54
+ * plain prose after `/loop` keeps starting an unbounded loop instead of erroring
55
+ * (the pre-arg-parsing behavior). Returns the error message string on failure.
56
+ */
57
+ export function parseLoopLimitArgs(args: string): ParsedLoopArgs | string {
58
+ const trimmed = args.trim();
59
+ if (!trimmed) return {};
60
+
61
+ const firstSpace = trimmed.search(/\s/);
62
+ const firstToken = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
63
+ const rest = firstSpace === -1 ? "" : trimmed.slice(firstSpace + 1).trim();
64
+ const token = firstToken.toLowerCase();
65
+
66
+ // Not a limit attempt (prose like "keep going") → unbounded loop, prompt = full args.
67
+ if (!/^[+-]?\d/.test(token)) {
68
+ return { prompt: trimmed };
52
69
  }
53
70
 
54
- const token = parts[0];
55
- const iterationMatch = /^(\d+)$/.exec(token);
56
- if (iterationMatch) {
57
- const iterations = Number(iterationMatch[1]);
58
- if (!Number.isSafeInteger(iterations) || iterations <= 0) {
59
- return "Loop count must be a positive integer.";
71
+ // Bare integer: iteration count, unless the next token is a time unit ("10 minutes").
72
+ if (/^\d+$/.test(token)) {
73
+ if (rest) {
74
+ const restTokens = rest.split(/\s+/);
75
+ const unitMs = TIME_UNITS_MS.get(restTokens[0].toLowerCase());
76
+ if (unitMs !== undefined) {
77
+ const limit = makeDuration(token, unitMs);
78
+ if (typeof limit === "string") return limit;
79
+ return { limit, prompt: restTokens.slice(1).join(" ").trim() || undefined };
80
+ }
60
81
  }
61
- return { kind: "iterations", iterations };
82
+ const limit = makeIterations(token);
83
+ if (typeof limit === "string") return limit;
84
+ return { limit, prompt: rest || undefined };
62
85
  }
63
86
 
64
- const durationMatch = /^(\d+)([a-z]+)$/.exec(token);
65
- if (durationMatch) {
66
- return parseDurationParts(durationMatch[1], durationMatch[2]);
87
+ // Compact / compound duration: "10m", "90s", "1h30m".
88
+ const duration = parseCompoundDuration(token);
89
+ if (duration !== undefined) {
90
+ if (typeof duration === "string") return duration;
91
+ return { limit: duration, prompt: rest || undefined };
67
92
  }
68
93
 
69
- return "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min.";
94
+ // Limit-shaped but unparseable ("-1", "1.5h", "10x10").
95
+ return LOOP_USAGE;
70
96
  }
71
97
 
72
- function parseDurationParts(amountText: string, unitText: string): LoopLimitConfig | string {
73
- if (!/^\d+$/.test(amountText)) {
74
- return "Loop duration must use a positive integer amount.";
98
+ function makeIterations(amountText: string): LoopLimitConfig | string {
99
+ const amount = Number(amountText);
100
+ if (!Number.isSafeInteger(amount) || amount <= 0) {
101
+ return "Loop count must be a positive integer.";
75
102
  }
103
+ return { kind: "iterations", iterations: amount };
104
+ }
76
105
 
106
+ function makeDuration(amountText: string, unitMs: number): LoopLimitConfig | string {
77
107
  const amount = Number(amountText);
78
108
  if (!Number.isSafeInteger(amount) || amount <= 0) {
79
109
  return "Loop duration must be positive.";
80
110
  }
111
+ return { kind: "duration", durationMs: amount * unitMs };
112
+ }
81
113
 
82
- const unitMs = TIME_UNITS_MS.get(unitText);
83
- if (unitMs === undefined) {
84
- return "Loop duration unit must be seconds, minutes, or hours.";
114
+ /**
115
+ * Parse a compact duration token such as `10m`, or a compound one like `1h30m`.
116
+ * Returns `undefined` when the token is not duration-shaped, or an error string
117
+ * when it is shaped like a duration but uses an unknown unit / non-positive
118
+ * amount.
119
+ */
120
+ function parseCompoundDuration(token: string): LoopLimitConfig | string | undefined {
121
+ if (!/^(?:\d+[a-z]+)+$/.test(token)) return undefined;
122
+ const segments = token.match(/\d+[a-z]+/g);
123
+ if (!segments) return undefined;
124
+ let totalMs = 0;
125
+ for (const segment of segments) {
126
+ const match = /^(\d+)([a-z]+)$/.exec(segment);
127
+ if (!match) return LOOP_USAGE;
128
+ const unitMs = TIME_UNITS_MS.get(match[2]);
129
+ if (unitMs === undefined) {
130
+ return "Loop duration unit must be seconds, minutes, or hours.";
131
+ }
132
+ const amount = Number(match[1]);
133
+ if (!Number.isSafeInteger(amount) || amount <= 0) {
134
+ return "Loop duration must be positive.";
135
+ }
136
+ totalMs += amount * unitMs;
85
137
  }
86
-
87
- return { kind: "duration", durationMs: amount * unitMs };
138
+ if (totalMs <= 0) return "Loop duration must be positive.";
139
+ return { kind: "duration", durationMs: totalMs };
88
140
  }
89
141
 
90
142
  export function createLoopLimitRuntime(
@@ -362,7 +362,7 @@ export interface InteractiveModeContext {
362
362
  handlePlanModeCommand(initialPrompt?: string): Promise<void>;
363
363
  handleGoalModeCommand(rest?: string): Promise<void>;
364
364
  handleGuidedGoalCommand(rest?: string): Promise<void>;
365
- handleLoopCommand(args?: string): Promise<void>;
365
+ handleLoopCommand(args?: string): Promise<string | undefined>;
366
366
  disableLoopMode(): void;
367
367
  pauseLoop(): void;
368
368
  handlePlanApproval(details: PlanApprovalDetails): Promise<void>;
@@ -22,7 +22,7 @@ Use tools whenever they improve correctness, completeness, or grounding.
22
22
  - SHOULD resolve prerequisites before acting.
23
23
  - NEVER stop at the first plausible answer if another call would cut uncertainty.
24
24
  - Empty, partial, or suspiciously narrow lookup? Retry a different strategy.
25
- - SHOULD parallelize independent calls.
25
+ - SHOULD parallelize calls when possible.
26
26
  {{#has tools "task"}}- User says `parallel`/`parallelize` → MUST use `{{toolRefs.task}}` subagents; parallel tool calls alone do not satisfy.{{/has}}
27
27
 
28
28
  # I/O
@@ -78,17 +78,28 @@ Pattern syntax (metavariables, `$$$` spreads) is in each tool's description.
78
78
  {{#has tools "task"}}
79
79
  # Eager Tasks
80
80
  {{#if eagerTasksAlways}}
81
- Delegation is the default, not the exception. Once the design is settled, you MUST fan work out to `{{toolRefs.task}}` subagents rather than doing it yourself. Work alone ONLY when one is unambiguously true:
82
- - a single-file edit under ~30 lines
83
- - a direct answer needing no code changes
84
- - the user explicitly asked you to run a command yourself
85
- Everything else — multi-file changes, refactors, features, tests, investigations — MUST be decomposed and delegated.{{#if taskBatch}} Batch independent slices into one parallel `{{toolRefs.task}}` call; never serialize what can run concurrently.{{/if}}
81
+ Delegation is the default here, not the exception. Once the design is settled, you MUST fan the work out to `{{toolRefs.task}}` subagents rather than doing it yourself. Work alone ONLY when one of these is unambiguously true:
82
+ - A single-file edit under ~30 lines
83
+ - A direct answer or explanation requiring no code changes
84
+ - The user explicitly asked you to run a command yourself
85
+ Everything else — multi-file changes, refactors, new features, tests, investigations — MUST be decomposed and delegated.{{#if taskBatch}} Batch independent slices into one parallel `{{toolRefs.task}}` call; never serialize what can run concurrently.{{/if}}
86
86
  {{else}}
87
- Delegation is preferred. Once the design is settled, you SHOULD fan substantial work out to `{{toolRefs.task}}` subagents — multi-file changes, refactors, features, tests, investigations are strong candidates. Use judgment for small, single-file, or interactive work.{{#if taskBatch}} Batch independent slices into one parallel `{{toolRefs.task}}` call rather than serializing them.{{/if}}
87
+ Delegation is preferred here. Once the design is settled, you SHOULD fan substantial work out to `{{toolRefs.task}}` subagents instead of doing everything yourself — multi-file changes, refactors, new features, tests, and investigations are strong candidates. Use your judgment for small, single-file, or interactive work.{{#if taskBatch}} When you delegate independent slices, batch them into one parallel `{{toolRefs.task}}` call rather than serializing them.{{/if}}
88
88
  {{/if}}
89
89
  {{/has}}
90
90
  {{/if}}
91
91
 
92
+ {{#has tools "task"}}
93
+ <parallel-reflex>
94
+ When work forks, you MUST fork. Guard against the sequential habit: comfort in one-thing-at-a-time, the illusion that order = correctness, the assumption that B depends on A.
95
+ ALWAYS use `{{toolRefs.task}}` to launch subagents when work forks into independent streams:
96
+ - editing 4+ files with no dependencies between edits
97
+ - investigating multiple subsystems
98
+ - work that decomposes into independent pieces
99
+ Sequential work MUST be justified. If you cannot articulate why B depends on A, you MUST parallelize.
100
+ </parallel-reflex>
101
+ {{/has}}
102
+
92
103
  {{#if toolInfo.length}}
93
104
  # Inventory
94
105
  {{#if mcpDiscoveryMode}}
@@ -1,68 +1,68 @@
1
- {{#if asyncEnabled}}{{#if batchEnabled}}Spawns subagents in the background — one per `tasks[]` item; single spawn = one-item batch.{{else}}Spawns ONE subagent per call in the background.{{/if}}
1
+ {{#if asyncEnabled}}{{#if batchEnabled}}Spawns subagents to work in the background — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Spawns ONE subagent per call to work in the background.{{/if}}
2
2
 
3
- - Non-blocking: returns agent id{{#if batchEnabled}}s{{/if}} + job id{{#if batchEnabled}}s{{/if}} immediately; each result auto-delivered on yield.
3
+ - Spawning is non-blocking: the call returns immediately with the agent id{{#if batchEnabled}}s{{/if}} and job id{{#if batchEnabled}}s{{/if}}; each result is delivered automatically when that agent yields.
4
4
  - Parallelism = {{#if batchEnabled}}multiple `tasks[]` items in ONE call. MUST batch into one `tasks[]` (share `context` once). Separate `task` calls ONLY for a different `agent` type or unrelated `context`{{else}}multiple `task` calls in one assistant message{{/if}}.
5
- - Blocked on a result? `job poll`; else keep working. `job cancel` kills a task, **cannot carry a message** — only for stalled/abandoned work.
6
- {{else}}{{#if batchEnabled}}Runs subagents synchronously — one per `tasks[]` item; single spawn = one-item batch.{{else}}Runs ONE subagent synchronously per call.{{/if}}
5
+ - If genuinely blocked on a result, wait with `job poll`; otherwise keep working. `job cancel` terminates a task and **cannot carry a message** — only for stalled/abandoned work.
6
+ {{else}}{{#if batchEnabled}}Runs subagents synchronously — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Runs ONE subagent synchronously per call.{{/if}}
7
7
 
8
- - Blocking: returns only after the agent{{#if batchEnabled}}s{{/if}} finish; results arrive inline.
8
+ - Spawning is blocking: the call returns only after the agent{{#if batchEnabled}}s{{/if}} finish; results arrive inline.
9
9
  - Parallelism = {{#if batchEnabled}}multiple `tasks[]` items in ONE call. MUST batch into one `tasks[]` (share `context` once). Separate `task` calls ONLY for a different `agent` type or unrelated `context`{{else}}multiple `task` calls in one assistant message{{/if}}.
10
10
  {{/if}}
11
11
  {{#if ircEnabled}}
12
- - Coordinate via `irc` by agent id; agents reach you + siblings live.
12
+ - Coordinate with agents via `irc` using their ids. Agents reach you and their siblings live the same way.
13
13
  {{/if}}
14
14
 
15
15
  <parameters>
16
16
  - `agent`: agent type to spawn
17
17
  {{#if batchEnabled}}
18
- - `context`: background prepended to every assignment — goal, constraints, contract (see context-fmt); REQUIRED, session-specific only
19
- - `tasks`: one subagent per item, all in parallel:
20
- - `assignment`: complete self-contained instructions; one-liners / missing acceptance criteria PROHIBITED
21
- - `id`: stable agent id, CamelCase, ≤32 chars; auto when omitted
18
+ - `context`: shared background prepended to every assignment — goal, constraints, shared contract (see context-fmt); REQUIRED, session-specific only
19
+ - `tasks`: tasks to spawn — one subagent per item, all in parallel:
20
+ - `assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED
21
+ - `id`: stable agent id, CamelCase, ≤32 chars; generated when omitted
22
22
  - `description`: UI label only — subagent never sees it
23
- - `role`: specialist identity (e.g. "Auth-flow security reviewer") — sets system-prompt persona + roster name
23
+ - `role`: specialist identity this subagent embodies (e.g. "Auth-flow security reviewer") — sets its system-prompt persona and roster display name; tailor every spawn rather than cloning a generic worker
24
24
  {{#if isolationEnabled}}
25
- - `isolated`: run spawn in isolated env; returns patches. Torn down at completion — not addressable after
25
+ - `isolated`: run this spawn in an isolated env; returns patches. Isolated agents are torn down at completion — not addressable afterwards
26
26
  {{/if}}
27
27
  {{else}}
28
- - `id`: stable agent id, CamelCase, ≤32 chars; auto when omitted
28
+ - `id`: stable agent id, CamelCase, ≤32 chars; generated when omitted
29
29
  - `description`: UI label only — subagent never sees it
30
- - `role`: specialist identity (e.g. "Auth-flow security reviewer") — sets system-prompt persona + roster name
31
- - `assignment`: complete self-contained instructions; one-liners / missing acceptance criteria PROHIBITED
30
+ - `role`: specialist identity this subagent embodies (e.g. "Auth-flow security reviewer") — sets its system-prompt persona and roster display name; tailor every spawn rather than cloning a generic worker
31
+ - `assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED
32
32
  {{#if isolationEnabled}}
33
- - `isolated`: run in isolated env; returns patches. Torn down at completion — not addressable after
33
+ - `isolated`: run in isolated env; returns patches. Isolated agents are torn down at completion — not addressable afterwards
34
34
  {{/if}}
35
35
  {{/if}}
36
36
  </parameters>
37
37
 
38
38
  <rules>
39
- - **Maximize fan-out.** Widest {{#if batchEnabled}}`tasks[]` batch{{else}}set of parallel `task` calls{{/if}} the work decomposes into. NEVER serialize parallelizable work.
40
- - **Subagents do not verify, lint, or format.** Each assignment MUST tell the subagent: skip all gates, formatters, project-wide build/test/lint. You run them once at the end across changed files.
39
+ - **Maximize fan-out.** Issue the widest {{#if batchEnabled}}`tasks[]` batch{{else}}set of parallel `task` calls{{/if}} the work decomposes into. NEVER serialize work that could run concurrently.
40
+ - **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates, formatters, and project-wide build/test/lint. You run them once at the end across the union of changed files.
41
41
  - No globs, no "update all", no package-wide scope. Fan out.
42
- - **Tailor every spawn with a `role`.** A named specialist (e.g. "Parser edge-case tester", "SSE backpressure specialist") beats a generic `task`/`quick_task` worker; decompose into specialists, never clones. Role-less spawn is the exception.
43
- - NEVER serialize over possible file overlap. Agents self-resolve collisions in real time.
44
- - Subagents have no conversation history. Every fact, file path, direction MUST be explicit in {{#if batchEnabled}}`context` or the item's `assignment`{{else}}the `assignment`{{/if}}.
42
+ - **Tailor every spawn with a `role`.** A role naming the specialist (e.g. "Parser edge-case tester", "SSE backpressure specialist") makes a sharper agent than a bare generic `task`/`quick_task` worker; decompose into named specialists, never clones of one generic worker. A role-less generic spawn is the exception.
43
+ - NEVER slow down or serialize because tasks might overlap on some files. Agents resolve collisions among themselves in real time.
44
+ - Subagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in {{#if batchEnabled}}`context` or the item's `assignment`{{else}}the `assignment`{{/if}}.
45
45
  {{#if batchEnabled}}
46
- - **Shared background** in `context` once, never per assignment. Large payloads via `local://<path>` URIs, not inline.
46
+ - **Shared background** lives in `context` once never duplicated across assignments. Pass large payloads via `local://<path>` URIs, not inline.
47
47
  {{else}}
48
- - **Shared background**: write ONCE to a `local://` file (e.g. `local://ctx.md`), reference it in each assignment. Large payloads via `local://<path>` URIs, not inline.
48
+ - **Shared background**: write it ONCE to a `local://` file (e.g. `local://ctx.md`) and reference that path in each assignment. Pass large payloads via `local://<path>` URIs, not inline.
49
49
  {{/if}}
50
- - Prefer agents that investigate **and** edit in one pass; spin a read-only discovery step only when affected files unknown.
51
- - **Read-only agents** (e.g. `explore`): no edit/write/command tools. NEVER assign them file changes or commands. Use to investigate + report; delegate edits to a writing agent (`task`/`oracle`/`designer`) or do them yourself.
52
- - **No reasoning offload**: NEVER route reasoning, analysis, design, or decisions to `quick_task`/`explore` — minimal-effort / small models for mechanical lookups + data collection only. Keep judgment + synthesis in your own context; delegate hard thinking to `task`/`plan`/`oracle`.
50
+ - Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.
51
+ - **Read-only agents**: Agents tagged READ-ONLY (e.g. `explore`) have no edit/write/command tools. NEVER hand them an assignment that requires changing files or running commands. Use them to investigate and report back; do the edits yourself or delegate to a writing agent (`task`, `oracle`, `designer`).
52
+ - **No reasoning offload**: NEVER offload reasoning, analysis, design, or decision-making to `quick_task` or `explore` — they run minimal-effort / small models for mechanical lookups and data collection only. Keep judgment and synthesis in your own context; delegate hard thinking to `task`, `plan`, or `oracle`.
53
53
  </rules>
54
54
 
55
55
  <parallelization>
56
56
  {{#if ircEnabled}}
57
- Test: can B run without A's output? No sequence A → B — **unless** B can ask A over `irc`. Live coordination beats a waterfall when the contract is small + DM-able.
58
- Still sequence when a task produces a large evolving contract (generated types, schema migration, core module API) consumed wholesale — IRC round-trips don't replace a finished artifact.
59
- Parallel when tasks touch disjoint files, are independent refactors/tests, or need only occasional peer clarification.
57
+ Test: can task B run correctly without seeing A's output? If no, sequence A → B — **unless** B can reasonably ask A for the missing piece over `irc`. Live coordination beats a serial waterfall when the contract is small and easy to describe in a DM.
58
+ Still sequence when one task produces a large, evolving contract (generated types, schema migration, core module API) the other consumes wholesale — IRC round-trips do not replace a finished artifact.
59
+ Parallel when tasks touch disjoint files, are independent refactors/tests, or only need occasional clarification that can be resolved peer-to-peer.
60
60
  {{else}}
61
- Test: can B run without A's output? No sequence A → B.
61
+ Test: can task B run correctly without seeing A's output? If no, sequence A → B.
62
62
  Sequential when one task produces a contract (types, API, schema, core module) the other consumes.
63
63
  Parallel when tasks touch disjoint files or are independent refactors/tests.
64
64
  {{/if}}
65
- {{#if ircEnabled}}Sequenced follow-ups SHOULD message the prerequisite's producer — it holds the context.{{/if}}
65
+ {{#if ircEnabled}}Sequenced follow-ups SHOULD message the agent that produced the prerequisite — it already holds the context.{{/if}}
66
66
  </parallelization>
67
67
 
68
68
  {{#if batchEnabled}}
@@ -306,11 +306,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
306
306
  name: "loop",
307
307
  description:
308
308
  "Toggle loop mode. While enabled, the next prompt you send re-submits after every yield. Esc cancels the current iteration; /loop again to disable.",
309
- inlineHint: "[count|duration]",
309
+ inlineHint: "[count|duration] [prompt]",
310
310
  allowArgs: true,
311
311
  handleTui: async (command, runtime) => {
312
- await runtime.ctx.handleLoopCommand(command.args);
312
+ const prompt = await runtime.ctx.handleLoopCommand(command.args);
313
313
  runtime.ctx.editor.setText("");
314
+ // Surface any inline prompt so the dispatcher returns it and the normal
315
+ // submit flow runs the first loop iteration (recording it as the loop prompt).
316
+ if (prompt) return { prompt };
314
317
  },
315
318
  },
316
319
  {