@oh-my-pi/pi-coding-agent 16.2.8 → 16.2.11

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/cli.js +3160 -3096
  3. package/dist/types/config/settings-schema.d.ts +41 -13
  4. package/dist/types/extensibility/skills.d.ts +29 -0
  5. package/dist/types/modes/components/mcp-add-wizard.d.ts +8 -0
  6. package/dist/types/modes/components/todo-reminder.d.ts +3 -1
  7. package/dist/types/modes/controllers/mcp-command-controller.d.ts +9 -0
  8. package/dist/types/modes/controllers/tool-args-reveal.d.ts +5 -0
  9. package/dist/types/modes/interactive-mode.d.ts +0 -1
  10. package/dist/types/modes/skill-command.d.ts +1 -1
  11. package/dist/types/modes/types.d.ts +0 -1
  12. package/dist/types/session/agent-session.d.ts +1 -1
  13. package/dist/types/stt/asr-client.d.ts +7 -3
  14. package/dist/types/stt/index.d.ts +1 -0
  15. package/dist/types/stt/stt-controller.d.ts +2 -0
  16. package/dist/types/stt/submit-trigger.d.ts +30 -0
  17. package/dist/types/task/index.d.ts +1 -1
  18. package/dist/types/task/types.d.ts +6 -6
  19. package/dist/types/tiny/models.d.ts +22 -8
  20. package/package.json +14 -13
  21. package/scripts/bundle-dist.ts +23 -4
  22. package/scripts/generate-docs-index.ts +116 -24
  23. package/src/async/job-manager.ts +27 -3
  24. package/src/cli/grep-cli.ts +1 -1
  25. package/src/commit/agentic/agent.ts +1 -1
  26. package/src/commit/agentic/prompts/system.md +1 -1
  27. package/src/commit/agentic/tools/analyze-file.ts +2 -2
  28. package/src/config/model-discovery.ts +118 -76
  29. package/src/config/settings-schema.ts +15 -1
  30. package/src/debug/profiler.ts +7 -1
  31. package/src/extensibility/skills.ts +77 -0
  32. package/src/internal-urls/docs-index.generated.txt +2 -2
  33. package/src/lsp/config.ts +17 -3
  34. package/src/mcp/oauth-flow.ts +35 -8
  35. package/src/modes/acp/acp-agent.ts +6 -9
  36. package/src/modes/components/mcp-add-wizard.ts +43 -3
  37. package/src/modes/components/model-selector.ts +21 -9
  38. package/src/modes/components/todo-reminder.ts +5 -1
  39. package/src/modes/controllers/event-controller.ts +40 -15
  40. package/src/modes/controllers/mcp-command-controller.ts +84 -3
  41. package/src/modes/controllers/selector-controller.ts +57 -35
  42. package/src/modes/controllers/tool-args-reveal.ts +12 -0
  43. package/src/modes/interactive-mode.ts +5 -10
  44. package/src/modes/rpc/rpc-mode.ts +5 -8
  45. package/src/modes/skill-command.ts +8 -20
  46. package/src/modes/types.ts +0 -1
  47. package/src/prompts/agents/tester.md +107 -0
  48. package/src/prompts/system/orchestrate-notice.md +2 -2
  49. package/src/prompts/system/system-prompt.md +2 -5
  50. package/src/prompts/system/thinking-loop-redirect.md +10 -0
  51. package/src/prompts/system/workflow-notice.md +1 -1
  52. package/src/prompts/tools/task.md +2 -9
  53. package/src/session/agent-session.ts +53 -18
  54. package/src/stt/asr-client.ts +87 -27
  55. package/src/stt/downloader.ts +8 -2
  56. package/src/stt/index.ts +1 -0
  57. package/src/stt/stt-controller.ts +31 -2
  58. package/src/stt/submit-trigger.ts +74 -0
  59. package/src/task/agents.ts +4 -4
  60. package/src/task/executor.ts +2 -4
  61. package/src/task/index.ts +32 -10
  62. package/src/task/types.ts +5 -5
  63. package/src/tiny/models.ts +10 -0
  64. package/src/tools/ast-grep.ts +34 -12
  65. package/src/tools/grep.ts +11 -8
  66. package/src/utils/git.ts +22 -1
  67. package/src/prompts/agents/oracle.md +0 -54
@@ -962,43 +962,65 @@ export class SelectorController {
962
962
  // every project's history when the cwd has nothing to resume. See #3099.
963
963
  const historyStorage = this.ctx.historyStorage;
964
964
  const historyMatcher = historyStorage ? (query: string) => historyStorage.matchingSessionIds(query) : undefined;
965
- this.showSelector(done => {
966
- const selector = new SessionSelectorComponent(
967
- sessions,
968
- async (session: SessionInfo) => {
969
- done();
970
- await this.handleResumeSession(session.path);
971
- },
972
- () => {
973
- done();
974
- this.ctx.ui.requestRender();
975
- },
976
- () => {
977
- void this.ctx.shutdown();
978
- },
979
- {
980
- onDelete: async (session: SessionInfo) => {
981
- if (!(await this.#detachActiveSessionBeforeDeletion(session.path))) {
982
- return false;
983
- }
984
- const storage = new FileSessionStorage();
985
- try {
986
- await storage.deleteSessionWithArtifacts(session.path);
987
- return true;
988
- } catch (err) {
989
- throw new Error(`Failed to delete session: ${err instanceof Error ? err.message : String(err)}`, {
990
- cause: err,
991
- });
992
- }
993
- },
994
- historyMatcher,
995
- loadAllSessions: () => SessionManager.listAll(),
996
- getTerminalRows: () => this.ctx.ui.terminal.rows,
965
+ // Fullscreen session picker on the alternate screen (the /settings idiom):
966
+ // the overlay borrows the alt buffer and enables mouse tracking (wheel
967
+ // scroll + click-to-resume) for its lifetime, leaving the transcript
968
+ // untouched underneath. Anchored top-left at full size so a mouse row maps
969
+ // directly to a rendered line (the overlay paints from screen row 0), and
970
+ // `fillHeight` pads the body so the footer pins to the screen bottom.
971
+ let overlayHandle: OverlayHandle | undefined;
972
+ const done = () => {
973
+ overlayHandle?.hide();
974
+ this.focusActiveEditorArea();
975
+ this.ctx.ui.requestRender();
976
+ };
977
+ const selector = new SessionSelectorComponent(
978
+ sessions,
979
+ async (session: SessionInfo) => {
980
+ done();
981
+ await this.handleResumeSession(session.path);
982
+ },
983
+ () => {
984
+ done();
985
+ },
986
+ () => {
987
+ // Release the alt buffer before teardown: shutdown() awaits flush/save/
988
+ // dispose/drain before stop() leaves the alt screen, so without this the
989
+ // fullscreen picker would freeze on screen for that window on Ctrl+C.
990
+ done();
991
+ void this.ctx.shutdown();
992
+ },
993
+ {
994
+ onDelete: async (session: SessionInfo) => {
995
+ if (!(await this.#detachActiveSessionBeforeDeletion(session.path))) {
996
+ return false;
997
+ }
998
+ const storage = new FileSessionStorage();
999
+ try {
1000
+ await storage.deleteSessionWithArtifacts(session.path);
1001
+ return true;
1002
+ } catch (err) {
1003
+ throw new Error(`Failed to delete session: ${err instanceof Error ? err.message : String(err)}`, {
1004
+ cause: err,
1005
+ });
1006
+ }
997
1007
  },
998
- );
999
- selector.setOnRequestRender(() => this.ctx.ui.requestRender());
1000
- return { component: selector, focus: selector };
1008
+ historyMatcher,
1009
+ loadAllSessions: () => SessionManager.listAll(),
1010
+ getTerminalRows: () => this.ctx.ui.terminal.rows,
1011
+ fillHeight: true,
1012
+ },
1013
+ );
1014
+ selector.setOnRequestRender(() => this.ctx.ui.requestRender());
1015
+ overlayHandle = this.ctx.ui.showOverlay(selector, {
1016
+ anchor: "top-left",
1017
+ width: "100%",
1018
+ maxHeight: "100%",
1019
+ margin: 0,
1020
+ fullscreen: true,
1001
1021
  });
1022
+ this.ctx.ui.setFocus(selector);
1023
+ this.ctx.ui.requestRender();
1002
1024
  }
1003
1025
 
1004
1026
  #refreshSessionTerminalTitle(): void {
@@ -177,6 +177,18 @@ export class ToolArgsRevealController {
177
177
  if (entry) entry.component = component;
178
178
  }
179
179
 
180
+ /** Migrate a live reveal entry from a placeholder key onto the real
181
+ * tool-call id once the owned-dialect parser assigns it. No-op when no
182
+ * entry exists under `from` (smoothing disabled, or the JSON already
183
+ * closed and `finish` cleared it). */
184
+ rekey(from: string, to: string): void {
185
+ if (from === to) return;
186
+ const entry = this.#entries.get(from);
187
+ if (!entry) return;
188
+ this.#entries.delete(from);
189
+ this.#entries.set(to, entry);
190
+ }
191
+
180
192
  /** Final arguments arrived (the JSON closed): drop the reveal so the
181
193
  * caller's final-args render wins immediately, mirroring how assistant
182
194
  * text snaps to the full message at message_end. */
@@ -100,6 +100,7 @@ import { formatDuration } from "../slash-commands/helpers/format";
100
100
  import { STTController, type SttState } from "../stt";
101
101
  import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
102
102
  import { formatTaskId } from "../task/render";
103
+ import type { ConfiguredThinkingLevel } from "../thinking";
103
104
  import type { LspStartupServerInfo } from "../tools";
104
105
  import { normalizeLocalScheme } from "../tools/path-utils";
105
106
  import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
@@ -378,7 +379,6 @@ export class InteractiveMode implements InteractiveModeContext {
378
379
  chatContainer: TranscriptContainer;
379
380
  pendingMessagesContainer: Container;
380
381
  statusContainer: Container;
381
- todoReminderContainer: Container;
382
382
  todoContainer: Container;
383
383
  subagentContainer: Container;
384
384
  btwContainer: Container;
@@ -493,8 +493,8 @@ export class InteractiveMode implements InteractiveModeContext {
493
493
  #goalTurnHadToolCalls = false;
494
494
  #goalContinuationTurnInFlight = false;
495
495
  #goalSuppressNextContinuation = false;
496
- #planModePreviousModelState: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
497
- #pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
496
+ #planModePreviousModelState: { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined;
497
+ #pendingModelSwitch: { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined;
498
498
  #planModeHasEntered = false;
499
499
  #planReviewOverlay: PlanReviewOverlay | undefined;
500
500
  #planReviewOverlayHandle: OverlayHandle | undefined;
@@ -547,7 +547,6 @@ export class InteractiveMode implements InteractiveModeContext {
547
547
  this.retryLoader = undefined;
548
548
  }
549
549
  this.statusContainer.clear();
550
- this.todoReminderContainer.clear();
551
550
  this.pendingMessagesContainer.clear();
552
551
  this.#cancelModelCycleClearTimer();
553
552
  this.modelCycleContainer.clear();
@@ -627,7 +626,6 @@ export class InteractiveMode implements InteractiveModeContext {
627
626
  this.chatContainer = new TranscriptContainer();
628
627
  this.pendingMessagesContainer = new Container();
629
628
  this.statusContainer = new AnchoredLiveContainer();
630
- this.todoReminderContainer = new AnchoredLiveContainer();
631
629
  this.todoContainer = new AnchoredLiveContainer();
632
630
  this.subagentContainer = new AnchoredLiveContainer();
633
631
  this.btwContainer = new AnchoredLiveContainer();
@@ -847,7 +845,6 @@ export class InteractiveMode implements InteractiveModeContext {
847
845
 
848
846
  this.ui.addChild(this.chatContainer);
849
847
  this.ui.addChild(this.pendingMessagesContainer);
850
- this.ui.addChild(this.todoReminderContainer);
851
848
  this.ui.addChild(this.todoContainer);
852
849
  this.ui.addChild(this.subagentContainer);
853
850
  this.ui.addChild(this.btwContainer);
@@ -1917,7 +1914,7 @@ export class InteractiveMode implements InteractiveModeContext {
1917
1914
  const planThinkingLevel = resolved.explicitThinkingLevel ? resolved.thinkingLevel : undefined;
1918
1915
 
1919
1916
  this.#planModePreviousModelState = currentModel
1920
- ? { model: currentModel, thinkingLevel: this.session.thinkingLevel }
1917
+ ? { model: currentModel, thinkingLevel: this.session.configuredThinkingLevel() }
1921
1918
  : undefined;
1922
1919
 
1923
1920
  if (!sameModel) {
@@ -2125,7 +2122,7 @@ export class InteractiveMode implements InteractiveModeContext {
2125
2122
  });
2126
2123
  }
2127
2124
 
2128
- async #restorePlanPreviousModel(prev: { model: Model; thinkingLevel?: ThinkingLevel }): Promise<void> {
2125
+ async #restorePlanPreviousModel(prev: { model: Model; thinkingLevel?: ConfiguredThinkingLevel }): Promise<void> {
2129
2126
  if (modelsAreEqual(this.session.model, prev.model)) {
2130
2127
  // Same model — only thinking level may differ. Avoid setModelTemporary()
2131
2128
  // which would reset provider-side sessions and break continuity.
@@ -4076,14 +4073,12 @@ export class InteractiveMode implements InteractiveModeContext {
4076
4073
  },
4077
4074
  ];
4078
4075
  }
4079
- this.todoReminderContainer.clear();
4080
4076
  this.#syncTodoAutoClearTimer();
4081
4077
  this.#renderTodoList();
4082
4078
  this.ui.requestRender();
4083
4079
  }
4084
4080
 
4085
4081
  async reloadTodos(): Promise<void> {
4086
- this.todoReminderContainer.clear();
4087
4082
  await this.#loadTodoList();
4088
4083
  this.ui.requestRender();
4089
4084
  }
@@ -22,7 +22,7 @@ import {
22
22
  type ExtensionWidgetOptions,
23
23
  getExtensionUISelectOptionLabel,
24
24
  } from "../../extensibility/extensions";
25
- import { buildSkillPromptMessage } from "../../extensibility/skills";
25
+ import { buildSkillPromptMessage, parseSkillInvocation } from "../../extensibility/skills";
26
26
  import { loadSlashCommands } from "../../extensibility/slash-commands";
27
27
  import { type Theme, theme } from "../../modes/theme/theme";
28
28
  import type { AgentSession } from "../../session/agent-session";
@@ -86,15 +86,12 @@ export async function tryRunRpcSkillCommand(
86
86
  session: RpcSkillCommandSession,
87
87
  text: string,
88
88
  ): Promise<RpcSkillCommandResult | false> {
89
- if (!text.startsWith("/skill:")) return false;
90
89
  if (!session.skillsSettings?.enableSkillCommands) return false;
91
- const spaceIndex = text.indexOf(" ");
92
- const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
93
- const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
94
- const skillName = commandName.slice("skill:".length);
95
- const skill = session.skills.find(candidate => candidate.name === skillName);
90
+ const parsed = parseSkillInvocation(text);
91
+ if (!parsed) return false;
92
+ const skill = session.skills.find(candidate => candidate.name === parsed.name);
96
93
  if (!skill) return false;
97
- const built = await buildSkillPromptMessage(skill, args);
94
+ const built = await buildSkillPromptMessage(skill, parsed.args);
98
95
  await session.promptCustomMessage({
99
96
  customType: SKILL_PROMPT_MESSAGE_TYPE,
100
97
  content: built.message,
@@ -1,4 +1,5 @@
1
1
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
2
+ import { getSkillSlashCommandName, parseSkillInvocation } from "../extensibility/skills";
2
3
  import { type CustomMessage, SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../session/messages";
3
4
  import type { InteractiveModeContext } from "./types";
4
5
 
@@ -20,11 +21,6 @@ type SkillPromptOptions = {
20
21
  queueChipText: string;
21
22
  };
22
23
 
23
- interface ParsedSkillCommand {
24
- commandName: string;
25
- args: string;
26
- }
27
-
28
24
  interface InvokeSkillCommandOptions {
29
25
  propagateErrors?: boolean;
30
26
  queueOnly?: boolean;
@@ -37,19 +33,11 @@ export interface BuiltSkillCommandPrompt {
37
33
  options: SkillPromptOptions;
38
34
  }
39
35
 
40
- function parseSkillCommand(text: string): ParsedSkillCommand | undefined {
41
- if (!text.startsWith("/skill:")) return undefined;
42
- const spaceIndex = text.indexOf(" ");
43
- const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
44
- const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
45
- return { commandName, args };
46
- }
47
-
48
- /** Return true when `text` names a registered `/skill:<name>` command. */
36
+ /** Return true when `text` invokes a registered `/skill:<name>` command. */
49
37
  export function isKnownSkillCommand(ctx: SkillCommandHost, text: string): boolean {
50
- const parsed = parseSkillCommand(text);
38
+ const parsed = parseSkillInvocation(text);
51
39
  if (!parsed) return false;
52
- return ctx.skillCommands.has(parsed.commandName);
40
+ return ctx.skillCommands.has(getSkillSlashCommandName({ name: parsed.name }));
53
41
  }
54
42
 
55
43
  /** Build the user-attributed custom message for a registered `/skill:<name>` command. */
@@ -59,9 +47,10 @@ export async function buildSkillCommandPrompt(
59
47
  streamingBehavior: "steer" | "followUp",
60
48
  images?: ImageContent[],
61
49
  ): Promise<BuiltSkillCommandPrompt | undefined> {
62
- const parsed = parseSkillCommand(text);
50
+ const parsed = parseSkillInvocation(text);
63
51
  if (!parsed) return undefined;
64
- const skillPath = ctx.skillCommands.get(parsed.commandName);
52
+ const commandName = getSkillSlashCommandName({ name: parsed.name });
53
+ const skillPath = ctx.skillCommands.get(commandName);
65
54
  if (!skillPath) return undefined;
66
55
 
67
56
  const content = await Bun.file(skillPath).text();
@@ -73,9 +62,8 @@ export async function buildSkillCommandPrompt(
73
62
  const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
74
63
  const textBlock: TextContent = { type: "text", text: message };
75
64
  const promptContent = images && images.length > 0 ? [textBlock, ...images] : message;
76
- const skillName = parsed.commandName.slice("skill:".length);
77
65
  const details: SkillPromptDetails = {
78
- name: skillName || parsed.commandName,
66
+ name: parsed.name,
79
67
  path: skillPath,
80
68
  args: parsed.args || undefined,
81
69
  lineCount: body ? body.split("\n").length : 0,
@@ -96,7 +96,6 @@ export interface InteractiveModeContext {
96
96
  chatContainer: TranscriptContainer;
97
97
  pendingMessagesContainer: Container;
98
98
  statusContainer: Container;
99
- todoReminderContainer: Container;
100
99
  todoContainer: Container;
101
100
  subagentContainer: Container;
102
101
  btwContainer: Container;
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: Tester
3
+ description: Authoritative test writer. ALWAYS delegate test authoring to this agent — NEVER write tests yourself. Writes high-signal tests defending real contracts (behavior, invariants, edge cases) and refuses worthless tests that assert plumbing or restate the code.
4
+ tools: read, grep, glob, bash, edit, write, lsp, ast_grep, ast_edit
5
+ spawns: explore
6
+ model: pi/task
7
+ thinking-level: high
8
+ ---
9
+
10
+ <system-conventions>
11
+ RFC 2119 applies to MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` and `AVOID` MUST be interpreted as aliases for `MUST NOT` and `SHOULD NOT` respectively.
12
+ </system-conventions>
13
+
14
+ You are a staff test engineer with taste. You write tests that earn their place in the suite and you delete — or refuse to write — tests that don't. You have agency: when asked for coverage that proves nothing, you write the test that would actually catch the bug instead.
15
+
16
+ <stakes>
17
+ A test suite is a liability until it pays for itself. Every worthless test is negative value: it costs CI time, blocks honest refactors, and lulls the team into false confidence while the real bug ships. A test's only job is to FAIL when behavior breaks and PASS otherwise. A test that cannot fail for any real defect is noise wearing a green check. You are here because models flood codebases with exactly that noise. You write the opposite.
18
+ </stakes>
19
+
20
+ <critical>
21
+ - The litmus for every test: **name the concrete, externally observable contract it defends** — a behavior, output shape, state transition, error mapping, invariant, or a regression-prone parsing boundary. Cannot name it in one sentence? NEVER write the test.
22
+ - Mutation test in your head: if a plausible bug — a flipped condition, an off-by-one, a wrong return value, a dropped case — would still let the test PASS, the test is worthless. Discard it.
23
+ - You NEVER write tests that assert plumbing or restate the implementation. The forbidden classes are enumerated in `<worthless-tests>` and are hard prohibitions.
24
+ - You MUST match the repo's existing test conventions — framework, file layout, naming, assertion style. A second convention beside an existing one is PROHIBITED.
25
+ </critical>
26
+
27
+ <anti-patterns name="worthless-tests">
28
+ NEVER write any of these. Each is a green check that survives real bugs:
29
+ - **Config/setter echo.** Setting a value then asserting it reads back (`set(x, 30); expect(get(x)).toBe(30)`) tests the language's assignment, not your code.
30
+ - **Source-grep.** Reading an implementation/build file and asserting on its TEXT — `expect(src).toContain("newFn()")`, `.toMatch(/import …/)`, `.not.toContain("oldName")`, "comment says X". Tests how code LOOKS, breaks on rename/reflow, passes while behavior is broken. Enforce structural facts with a type test or lint rule; enforce behavior by running the code.
31
+ - **Tautologies.** `expect(true).toBe(true)`, `expect(x).toBe(x)`, asserting a constant equals its literal.
32
+ - **Bare no-throw.** `expect(() => f()).not.toThrow()` with no assertion on the result. "It ran" is not a contract.
33
+ - **Construction smoke.** "Constructs without error", "package boots", "command starts" — unless that wiring genuinely can't be exercised in-process AND a real failure mode hides there.
34
+ - **Mock round-trips.** Asserting a mock was called with the args you just passed it. You tested the mock, not the system.
35
+ - **Existence/shape-only.** Non-empty string, length-grew, "field is defined", "returns an object with key Y" — without asserting the VALUE that matters.
36
+ - **Default snapshots.** Asserting every field of a default config equals its current default. A harmless default change shouldn't redden a test. Assert logical behavior, not the current state.
37
+ - **Field-wiring.** Asserting an option passed in lands on a property, or that a getter returns the value the constructor stored. Test the downstream BEHAVIOR that depends on it, not the assignment.
38
+ - **Duplicate-layer coverage.** Re-proving through mocks what an integration test already proves. Drop the narrower restatement.
39
+
40
+ When asked for coverage that would only produce the above, you write the test that actually exercises the behavior, and you state in your result why the requested shape was worthless.
41
+ </anti-patterns>
42
+
43
+ <what-to-test>
44
+ Aim every test at something that can actually break:
45
+ - **Behavior & outputs** — given input, the observable result (return value, emitted event, written file, error surfaced).
46
+ - **State transitions** — the legal and illegal moves of a stateful component; one test per invariant or transition, not one per field touched.
47
+ - **Invariants across fields** — relationships that MUST hold (sorted output stays sorted, sum of parts equals total, encode∘decode is identity).
48
+ - **Edge & boundary values** — zero, empty, one, max, negative, off-by-one, overflow, unicode, the value just inside and just outside a limit.
49
+ - **Precedence & resolution** — arg beats env beats default; later override wins; first-match-wins.
50
+ - **Error paths** — trigger the REAL failure (bad input, missing dep, denied permission) and assert the surfaced contract (error type, message mapping, exit code). NEVER instantiate the error class directly or inspect internal metadata.
51
+ - **Regression-prone parsing boundaries** — the exact bytes where a parser/serializer historically broke; pin past regressions with a named case.
52
+ </what-to-test>
53
+
54
+ <techniques>
55
+ Reach for the right shape; do not reinvent what the repo's framework already gives you.
56
+ - **Table-driven tests.** One body, many `{ name, input, expected }` rows covering boundaries and equivalence classes plus error cases. Name every row so a failure points at the case. The default shape for any function with a clear input→output mapping.
57
+ - **Subtests.** Group related cases under one parent with isolated setup and independent failure reporting. Prefer over many tiny near-duplicate test functions.
58
+ - **Property-based tests.** Assert invariants over generated inputs — round-trip identity, idempotence (`f(f(x)) == f(x)`), commutativity, monotonicity, "never panics and output stays well-formed". Catches cases you wouldn't enumerate by hand.
59
+ - **Deterministic randomness.** Seed every generator and PRINT the seed on failure so a red run reproduces exactly. NEVER use an unseeded clock-derived source — flaky tests are worse than no tests.
60
+ - **Fuzz tests.** For parsers, decoders, deserializers, anything eating untrusted bytes: feed mutated/random input, assert no crash and that invariants hold. Seed the corpus from known-tricky inputs and every past regression.
61
+ - **Benchmarks.** ONLY when performance is part of the contract. Measure the operation, not setup; consume the result so it isn't optimized away; compare against a baseline or threshold. A benchmark that asserts nothing is documentation, not a test.
62
+ - **Golden/snapshot.** Only for genuinely stable, human-reviewed output where exact bytes are the contract (codegen, serialized formats). NEVER snapshot volatile or incidental output — it becomes a rubber stamp nobody reads.
63
+ </techniques>
64
+
65
+ <black-box>
66
+ - **Test through the public API**, the way a real consumer calls it. Place tests in an EXTERNAL test package/module (separate namespace, no access to internals) so the compiler forbids reaching past the contract. This is the default and it forces you to test what callers depend on.
67
+ - **Internal (white-box) tests only for private invariants with no observable surface** — e.g. a balancing property of an internal tree, a cache eviction order. Justify each one; if the invariant has an observable effect, test that effect from outside instead.
68
+ - NEVER reach into private state to assert what you could observe through the public surface. Coupling tests to internals is what makes refactors painful and tempts people to delete the suite.
69
+ </black-box>
70
+
71
+ <fakes>
72
+ - **Prefer real implementations.** If the dependency is cheap and deterministic, use the real thing.
73
+ - **Prefer hand-written fakes over mocking frameworks.** A small in-memory implementation of an interface is type-checked, readable, survives refactors, and tests behavior. Mocking frameworks pull you toward asserting call counts and argument sequences — that is plumbing, and it breaks on every harmless internal change.
74
+ - **Mock only true external boundaries** — network, wall clock, filesystem, system randomness, third-party services — and even there a fake beats a mock. Inject the boundary; never patch globals.
75
+ - NEVER use module-registry mocking that leaks across test files. Spy on the imported object and restore in teardown.
76
+ </fakes>
77
+
78
+ <isolation>
79
+ Tests MUST be full-suite safe and order-independent, not merely file-local safe.
80
+ - **No timing dependence.** NEVER `sleep`/`setTimeout`-race to "let it settle". Inject a controllable clock and advance it; wait on a condition, signal, or promise, never a wall-clock duration. Real-time waits are the #1 source of flake.
81
+ - **No environment pollution.** NEVER leak env vars, temp files, global singletons, `process.env`/`process.platform`/`Bun.*` mutations, or monkeypatches past the test. Use per-test setup with restore in teardown. A test that passes alone but poisons a later file is broken.
82
+ - **Deterministic.** No dependence on map/iteration order, filesystem ordering, locale, timezone, or concurrency interleaving unless that ordering IS the contract under test.
83
+ - **Hermetic.** No real network or real time. Each test creates and tears down its own fixtures.
84
+ </isolation>
85
+
86
+ <workflow>
87
+ 1. **Study the code under test.** Read exact signatures, return types, and error paths with `lsp`/`read` — NEVER guess an API. Spawn `explore` for unfamiliar areas.
88
+ 2. **Study existing tests.** Find the framework, file layout, naming, fake/fixture helpers, and assertion style. You MUST reuse them. `grep`/`glob` for sibling test files.
89
+ 3. **Enumerate contracts.** List the observable behaviors, invariants, edge cases, and error mappings worth defending. Drop anything that fails the `<critical>` litmus.
90
+ 4. **Pick the shape** per `<techniques>` — table, property, fuzz, benchmark, or a focused unit/integration test.
91
+ 5. **Write the tests**, matching repo conventions exactly. Assert semantic content; assert exact bytes ONLY where downstream parses them.
92
+ 6. **Run them and verify they have teeth.** Execute the suite with the repo's runner; confirm green. Then confirm each test can FAIL: mentally (or by a throwaway mutation) check that a real defect reddens it. A test you never saw fail is unproven.
93
+ </workflow>
94
+
95
+ <verify>
96
+ - You MUST run the tests you wrote with the project's test command and confirm they pass.
97
+ - You MUST confirm they are not vacuous: a test that passes against broken code is a defect you authored. When cheap, perturb the implementation to watch the test fail, then revert.
98
+ - Run ONLY the tests you added or touched unless asked for the full suite.
99
+ - Report each test by the contract it defends — not "added N tests", but "covers <behavior/invariant/edge>".
100
+ </verify>
101
+
102
+ <critical>
103
+ - A test exists to FAIL on a real bug. No nameable contract, or no plausible bug would redden it → NEVER write it.
104
+ - NEVER assert plumbing, restate the implementation, or grep the source. Test observable behavior through the public surface.
105
+ - No timing races, no environment pollution, deterministic and order-independent — full-suite safe.
106
+ - You MUST keep going until the tests are written, passing, and proven to have teeth.
107
+ </critical>
@@ -15,7 +15,7 @@ You decompose, dispatch, verify, and iterate. Substantial and parallelizable wor
15
15
  7. **Respawn, do not absorb.** If a subagent returns incomplete or wrong work, spawn a corrective subagent with the specific gap — NEVER silently fix it yourself.
16
16
  8. **No scope creep, no scope shrink.** NEVER add work the user did not ask for. NEVER relabel unfinished items as "follow-up", "v1", or "MVP" to imply completion.
17
17
  9. **Subagents do not verify, lint, or format.** Every `task` assignment MUST instruct the subagent to skip all gates and formatters. Their job is the edit only. You — the orchestrator — run verification and formatting **once** at the end of the phase across the union of changed files. Avoids redundant runs and racing formatter passes.
18
- 10. **Right-size the offload — do not micro-task.** Subagents are for substantial or parallelizable chunks, not every keystroke. A trivial, self-contained mechanical edit — deleting a redundant glob, fixing one line in a config, renaming a single symbol in one file — costs less to *do* than to describe in a Goal/Constraints assignment. Make those yourself with `edit`/`write` and move on; reserve `task`/`quick_task` for work large enough to justify the dispatch overhead.
18
+ 10. **Right-size the offload — do not micro-task.** Subagents are for substantial or parallelizable chunks, not every keystroke. A trivial, self-contained mechanical edit — deleting a redundant glob, fixing one line in a config, renaming a single symbol in one file — costs less to *do* than to describe in a Goal/Constraints assignment. Make those yourself with `edit`/`write` and move on; reserve `task`/`sonic` for work large enough to justify the dispatch overhead.
19
19
  </rules>
20
20
 
21
21
  <workflow>
@@ -30,7 +30,7 @@ You decompose, dispatch, verify, and iterate. Substantial and parallelizable wor
30
30
 
31
31
  <anti-patterns>
32
32
  - Doing substantial or parallelizable work yourself instead of fanning it out to subagents.
33
- - Wrapping a single trivial edit (e.g. removing one redundant config line) in a `task`/`quick_task` with full Goal/Constraints scaffolding — just make the edit inline.
33
+ - Wrapping a single trivial edit (e.g. removing one redundant config line) in a `task`/`sonic` with full Goal/Constraints scaffolding — just make the edit inline.
34
34
  - Yielding after phase 1 with "ready to continue?".
35
35
  - Dispatching one subagent at a time when five could run in parallel.
36
36
  - Skipping `bun check` between phases because "the change looked safe".
@@ -184,9 +184,7 @@ EXECUTION WORKFLOW
184
184
 
185
185
  # 5. Verify
186
186
  - NEVER yield non-trivial work without proof: tests, E2E, browsing, or QA. Run only tests you added or modified unless asked otherwise.
187
- - Prefer unit or runnable E2E tests. NEVER create mocks.
188
- - Test behavior, not plumbing—things that can actually break.
189
- - Don't test defaults: a config or string change shouldn't break the test. Assert logical behavior, not current state.
187
+ - Test behavior, using tester agent where available. Assert logical behavior, not current state.
190
188
  - Aim at conditional branches, edge values, invariants across fields, and error handling versus silent broken results.
191
189
 
192
190
  # 6. Cleanup
@@ -201,7 +199,6 @@ DELIVERY CONTRACT
201
199
  <contract>
202
200
  Inviolable.
203
201
  - NEVER yield unless the deliverable is complete. A phase boundary, todo flip, or sub-step is NEVER a yield point—continue in the same turn.
204
- - NEVER suppress tests to make code pass.
205
202
  - NEVER fabricate outputs. Claims about code, tools, tests, docs, or sources MUST be grounded.
206
203
  - NEVER substitute an easier or more familiar problem:
207
204
  - Don't infer extra scope—retries, validation, telemetry, abstraction “while you're at it”—because it changes the contract.
@@ -223,7 +220,7 @@ Inviolable.
223
220
  - Output format MUST match the ask.
224
221
  - Every claim about code, tools, tests, docs, or sources MUST be grounded.
225
222
  - Mark any claim not directly observed or established as `[INFERENCE]`.
226
- - Verification claims MUST match what was exercised. Build, typecheck, lint, or unit-of-one tests don't prove integrations, performance, parity, or untested branches.
223
+ - Verification claims MUST match what was exercised, preferably smoke tested.
227
224
  - No required tool lookup may be skipped when it would cut uncertainty.
228
225
  - Be brief in prose, not in evidence, verification, or blocking details.
229
226
  </evidence-and-output>
@@ -0,0 +1,10 @@
1
+ <system-interrupt reason="thinking_loop_detected">
2
+ The loop guard interrupted your previous turn: your reasoning or response repeated near-identical content without making progress. Re-sampling the same context kept producing the same loop, so this is a corrective notice — not a prompt injection.
3
+
4
+ Restating the same plan, summary, or intention again will loop again. Break the pattern now:
5
+ - STOP narrating what you are about to do. Issue one concrete tool call that performs the smallest real next step, using your normal tool-calling format.
6
+ - If you were stuck deciding between options, pick the most boring viable one and act; do not deliberate further.
7
+ - If the task is genuinely complete, emit your final answer instead of more reasoning.
8
+
9
+ Do something different from the looped content. Act, don't re-plan.
10
+ </system-interrupt>
@@ -13,7 +13,7 @@ Worth it when the task benefits from decomposition + parallel coverage, or from
13
13
  <helpers>
14
14
  State persists across eval calls, so scout in one call and fan out in the next. Every eval call has:
15
15
 
16
- - `agent(prompt, *, agent="task", model=None, label=None, schema=None, isolated=None, apply=None, merge=None, handle=False)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent` picks a discovered agent ("explore", "reviewer", "oracle", …); `label` names the artifact. Shared background goes in a `local://` file referenced from each prompt, not a parameter. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes; eval-spawned agents nest at most 3 deep. Pass `isolated=True` to run the spawn in a copy-on-write worktree so parallel `agent()` calls can edit overlapping files safely — strict opt-in, mirrors the `task` tool, defaults off regardless of `task.isolation.mode`; `isolated=True` while the setting is `"none"` errors out instead of silently downgrading. With isolation, `apply=False` keeps changes in the worktree, and `merge=False` forces patch mode even when the setting is `"branch"`. Captured root patch path, branch name, nested repo patches, and apply summary reach the workflow through `handle=True` — combine it with `apply=False` (or `apply=False, schema=…`) and read `node["patch_path"]`, `node["branch_name"]`, `node["nested_patches"]`, `node["changes_applied"]`, `node["isolation_summary"]` (JS: same keys camelCased) to recover artifacts.
16
+ - `agent(prompt, *, agent="task", model=None, label=None, schema=None, isolated=None, apply=None, merge=None, handle=False)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent` picks a discovered agent ("explore", "reviewer", …); `label` names the artifact. Shared background goes in a `local://` file referenced from each prompt, not a parameter. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes; eval-spawned agents nest at most 3 deep. Pass `isolated=True` to run the spawn in a copy-on-write worktree so parallel `agent()` calls can edit overlapping files safely — strict opt-in, mirrors the `task` tool, defaults off regardless of `task.isolation.mode`; `isolated=True` while the setting is `"none"` errors out instead of silently downgrading. With isolation, `apply=False` keeps changes in the worktree, and `merge=False` forces patch mode even when the setting is `"branch"`. Captured root patch path, branch name, nested repo patches, and apply summary reach the workflow through `handle=True` — combine it with `apply=False` (or `apply=False, schema=…`) and read `node["patch_path"]`, `node["branch_name"]`, `node["nested_patches"]`, `node["changes_applied"]`, `node["isolation_summary"]` (JS: same keys camelCased) to recover artifacts.
17
17
  - `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool is bounded by the session's `task` concurrency — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one.
18
18
  - `pipeline(items, *stages)` — map items through `stages` left-to-right. There is a BARRIER between stages: ALL items clear stage N before stage N+1 begins. Each stage is a one-arg callable; stage 1 gets the original item, later stages get the previous result. Same pool width as `parallel()`.
19
19
  - `completion(prompt, *, model="default", system=None, schema=None)` — oneshot, stateless model call (no tools, no history). Tiers: "smol", "default", "slow". Cheap classification/scoring inside a fan-out.
@@ -7,11 +7,10 @@ Execution blocks your turn: the call only returns once the work is completely fi
7
7
  - **Sequence only when necessary:** The only reason to run A before B is if B strictly requires A's output to function (e.g., a core API contract or schema migration). {{#if ircEnabled}}If the missing piece is small, run them in parallel and have B ask A via `irc`!{{/if}}
8
8
  - **Role matching:** Assign each subagent a specific `role` (e.g. "Security Reviewer", "DB Migrator"). Do not spawn generic workers.
9
9
  - **No overhead:** Each assignment MUST instruct its agent to skip formatters, linters, and project-wide test suites. You will run those once at the end.
10
- - **Do your own thinking:** NEVER assign reasoning, architecture, or design to `quick_task` or `explore`. They are for mechanical lookups only. Keep hard decisions in your own context or use `task`, `plan`, or `oracle`.
11
10
  - **One-pass agents:** Prefer agents that investigate **and** edit in a single pass; only spin a read-only discovery step (e.g. `explore`) when the affected files are genuinely unknown.
12
11
 
13
12
  # Inputs
14
- - `agent`: The base agent type to use (e.g., `task`, `explore`).
13
+ - `agent` (optional): The base agent type to use (e.g., `explore`, `reviewer`). Defaults to `task` (the general-purpose worker) — omit it for the default worker instead of passing `agent: "task"`.
15
14
  {{#if batchEnabled}}
16
15
  - `context`: Shared project state, constraints, and contracts. Applies to the entire batch; do not duplicate this background into individual tasks.
17
16
  - `tasks[]`: Array of subagents to spawn.
@@ -37,13 +36,7 @@ Subagents start blank. They have no access to your conversation history.
37
36
  {{#if batchEnabled}}
38
37
  - Pass large payloads using `local://<path>` URIs, never inline text.
39
38
  {{else}}
40
- - *Note: The single-spawn shape has no `context` field.* Write shared project state ONCE to a `local://` file (e.g., `local://ctx.md`) and reference that URL in your assignments. Pass large payloads using `local://<path>` URIs, never inline text.
41
- {{/if}}
42
- {{#if ircEnabled}}
43
- - Once spawned, coordinate with live agents via `irc` using their IDs. If task B depends on task A, B SHOULD message A directly.
44
- {{/if}}
45
- {{#if asyncEnabled}}
46
- - If you run out of things to do and are genuinely blocked waiting for a subagent, use `job poll`. Use `job cancel` only for stalled work.
39
+ - Write shared project state ONCE to a `local://` file (e.g., `local://ctx.md`) and reference that URL in your assignments.
47
40
  {{/if}}
48
41
 
49
42
  # Format Contracts