@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.
- package/CHANGELOG.md +7 -0
- package/dist/cli.js +2830 -2819
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/loop-limit.d.ts +14 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/modes/interactive-mode.ts +15 -9
- package/src/modes/loop-limit.ts +80 -28
- package/src/modes/types.ts +1 -1
- package/src/prompts/system/system-prompt.md +18 -7
- package/src/prompts/tools/task.md +31 -31
- package/src/slash-commands/builtin-registry.ts +5 -2
|
@@ -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<
|
|
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
|
|
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<
|
|
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.
|
|
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.
|
|
52
|
-
"@oh-my-pi/omp-stats": "16.1.
|
|
53
|
-
"@oh-my-pi/pi-agent-core": "16.1.
|
|
54
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
55
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
56
|
-
"@oh-my-pi/pi-mnemopi": "16.1.
|
|
57
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
58
|
-
"@oh-my-pi/pi-tui": "16.1.
|
|
59
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
60
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
61
|
-
"@oh-my-pi/snapcompact": "16.1.
|
|
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<
|
|
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
|
|
1137
|
-
if (typeof
|
|
1138
|
-
this.showError(
|
|
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(
|
|
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 =
|
|
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}
|
|
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 {
|
package/src/modes/loop-limit.ts
CHANGED
|
@@ -38,53 +38,105 @@ const TIME_UNITS_MS = new Map<string, number>([
|
|
|
38
38
|
["hours", 3_600_000],
|
|
39
39
|
]);
|
|
40
40
|
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
82
|
+
const limit = makeIterations(token);
|
|
83
|
+
if (typeof limit === "string") return limit;
|
|
84
|
+
return { limit, prompt: rest || undefined };
|
|
62
85
|
}
|
|
63
86
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
94
|
+
// Limit-shaped but unparseable ("-1", "1.5h", "10x10").
|
|
95
|
+
return LOOP_USAGE;
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
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(
|
package/src/modes/types.ts
CHANGED
|
@@ -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<
|
|
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
|
|
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
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
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}}
|
|
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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
6
|
-
{{else}}{{#if batchEnabled}}Runs subagents synchronously — one per `tasks[]` item; single spawn
|
|
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
|
-
-
|
|
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`
|
|
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
|
|
21
|
-
- `id`: stable agent id, CamelCase, ≤32 chars;
|
|
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
|
|
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.
|
|
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;
|
|
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
|
|
31
|
-
- `assignment`: complete self-contained instructions; one-liners
|
|
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.
|
|
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.**
|
|
40
|
-
- **Subagents do not verify, lint, or format.**
|
|
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
|
|
43
|
-
- NEVER serialize
|
|
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
|
|
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`)
|
|
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
|
|
51
|
-
- **Read-only agents
|
|
52
|
-
- **No reasoning offload**: NEVER
|
|
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?
|
|
58
|
-
Still sequence when
|
|
59
|
-
Parallel when tasks touch disjoint files, are independent refactors/tests, or need
|
|
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?
|
|
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
|
|
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
|
{
|