@oh-my-pi/pi-coding-agent 16.3.13 → 16.3.15

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.
@@ -1,5 +1,10 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  export declare function canonicalizeMessage(text: string | null | undefined): string;
3
+ /**
4
+ * Thinking text prepared for display. Both modes drop empty `<!-- -->`
5
+ * sentinel lines outside code fences (see {@link isCommentNoise}); prose-only
6
+ * mode additionally elides fenced code down to a trailing ellipsis.
7
+ */
3
8
  export declare function formatThinkingForDisplay(text: string, proseOnly: boolean): string;
4
9
  /** Whether a formatted thinking block has non-placeholder content worth rendering. */
5
10
  export declare function hasDisplayableThinking(text: string | null | undefined, formattedText: string | null | undefined): boolean;
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.3.13",
4
+ "version": "16.3.15",
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",
@@ -56,17 +56,17 @@
56
56
  "@agentclientprotocol/sdk": "0.25.0",
57
57
  "@babel/parser": "^7.29.7",
58
58
  "@mozilla/readability": "^0.6.0",
59
- "@oh-my-pi/hashline": "16.3.13",
60
- "@oh-my-pi/omp-stats": "16.3.13",
61
- "@oh-my-pi/pi-agent-core": "16.3.13",
62
- "@oh-my-pi/pi-ai": "16.3.13",
63
- "@oh-my-pi/pi-catalog": "16.3.13",
64
- "@oh-my-pi/pi-mnemopi": "16.3.13",
65
- "@oh-my-pi/pi-natives": "16.3.13",
66
- "@oh-my-pi/pi-tui": "16.3.13",
67
- "@oh-my-pi/pi-utils": "16.3.13",
68
- "@oh-my-pi/pi-wire": "16.3.13",
69
- "@oh-my-pi/snapcompact": "16.3.13",
59
+ "@oh-my-pi/hashline": "16.3.15",
60
+ "@oh-my-pi/omp-stats": "16.3.15",
61
+ "@oh-my-pi/pi-agent-core": "16.3.15",
62
+ "@oh-my-pi/pi-ai": "16.3.15",
63
+ "@oh-my-pi/pi-catalog": "16.3.15",
64
+ "@oh-my-pi/pi-mnemopi": "16.3.15",
65
+ "@oh-my-pi/pi-natives": "16.3.15",
66
+ "@oh-my-pi/pi-tui": "16.3.15",
67
+ "@oh-my-pi/pi-utils": "16.3.15",
68
+ "@oh-my-pi/pi-wire": "16.3.15",
69
+ "@oh-my-pi/snapcompact": "16.3.15",
70
70
  "@opentelemetry/api": "^1.9.1",
71
71
  "@opentelemetry/context-async-hooks": "^2.7.1",
72
72
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -40,6 +40,18 @@ interface FinalizableBlock {
40
40
  * commits until the block finalizes.
41
41
  */
42
42
  getTranscriptBlockSettledRows?(): number;
43
+ /**
44
+ * Whether the block is a displaceable snapshot (todo/poll card) kept
45
+ * unfinalized only so a follow-up matching call can retract it. Paired
46
+ * with {@link seal}: once any of its rows enters native scrollback the
47
+ * container seals it — rows on the tape are immutable, so retraction is
48
+ * no longer possible, and an unfinalized block would otherwise pin the
49
+ * live-region seam open for the rest of the turn (every row committed
50
+ * below it audit-exempt, mass-recommitted when it finally finalizes).
51
+ */
52
+ isDisplaceableBlock?(): boolean;
53
+ /** Finalize a displaceable snapshot in place (settle animation, freeze bytes). */
54
+ seal?(): void;
43
55
  }
44
56
 
45
57
  function isBlockFinalized(child: Component): boolean {
@@ -60,6 +72,12 @@ function getBlockSettledRows(child: Component): number {
60
72
  return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
61
73
  }
62
74
 
75
+ /** Seal a displaceable snapshot whose rows entered native scrollback (see {@link FinalizableBlock.isDisplaceableBlock}). */
76
+ function sealCommittedSnapshot(child: Component): void {
77
+ const block = child as Component & FinalizableBlock;
78
+ if (block.isDisplaceableBlock?.()) block.seal?.();
79
+ }
80
+
63
81
  // A "plain blank" row is empty or whitespace-only with no ANSI bytes. It marks
64
82
  // separation padding (a `Spacer`, or a no-background `paddingY` row) as opposed
65
83
  // to a background-colored padding row, whose escape sequences contain `\S` and
@@ -275,6 +293,21 @@ export class TranscriptContainer
275
293
 
276
294
  const count = this.children.length;
277
295
 
296
+ // Seal displaceable snapshots whose rows are already on the tape (per the
297
+ // previous frame's segments — the geometry the committed count was
298
+ // computed against): immutable history can no longer be retracted, and
299
+ // left unfinalized such a block would pin the live-region seam open below
300
+ // it. Runs before the live-block scan so the seam unpins in this same
301
+ // frame, and every frame so a block that BECAME displaceable after its
302
+ // pending-preview rows committed (late result on a scrolled-off call) is
303
+ // caught too.
304
+ for (let i = 0; i < count && i < this.#segments.length; i++) {
305
+ const previous = this.#segments[i]!;
306
+ if (previous.startRow >= this.#committedRows) break;
307
+ if (previous.rowCount === 0 || previous.component !== this.children[i]) continue;
308
+ sealCommittedSnapshot(previous.component);
309
+ }
310
+
278
311
  // The commit boundary stops at the earliest still-mutating block. A
279
312
  // block that has not finalized must gate it: out-of-band inserts
280
313
  // (TTSR/todo cards) can append a finalized block *below* a tool that is
@@ -182,9 +182,11 @@ EXECUTION WORKFLOW
182
182
  {{#has tools "ask"}}- Ask before destructive commands or deleting code you didn't write.{{else}}- Don't run destructive git commands or delete code you didn't write.{{/has}}
183
183
 
184
184
  # 5. Verify
185
- - NEVER yield non-trivial work without proof: tests, E2E, browsing, or QA. Run only tests you added or modified unless asked otherwise.
186
- - Test behavior, using tester agent where available. Assert logical behavior, not current state.
187
- - Aim at conditional branches, edge values, invariants across fields, and error handling versus silent broken results.
185
+ - NEVER yield non-trivial work without proof: tests, E2E, browsing, or QA.
186
+ - Every test MUST defend an observable contract and fail on a plausible bug.
187
+ - Test behavior, boundaries, invariants, transitions, precedence, and real errors—not plumbing, source text, or incidental defaults.
188
+ - Match existing conventions; keep tests deterministic, isolated, and full-suite safe.
189
+ - Run only touched tests; small/no-test changes still REQUIRE a focused behavioral smoke test.
188
190
 
189
191
  # 6. Cleanup
190
192
  Changelog, tests, docs, and removing scaffolding are the LAST phase—NEVER skipped, but gated on the request demonstrably working.
@@ -51,20 +51,20 @@ Decompose first, then {{#if taskBatch}}batch the independent leaves{{else}}issue
51
51
 
52
52
  {{#if taskBatch}}
53
53
  task(
54
- context: "# Goal\nReview the auth diff...\n# Constraints\nRead-only...\n# Contract\nReturn findings as severity/file/line/fix...",
54
+ context: "# Goal\nReview the auth diff…\n# Constraints\nRead-only…\n# Contract\nReturn findings as severity/file/line/fix",
55
55
  tasks: [
56
- { id: "AuthOwner", role: "Auth Storage Reviewer", assignment: "# Target\npackages/ai/src/auth-storage.ts\n# Change\nTrace credential selection...\n# Acceptance\nReturn confirmed findings only..." },
57
- { id: "PromptOwner", role: "Prompt Contract Reviewer", assignment: "# Target\npackages/coding-agent/src/prompts/**\n# Change\nCheck active-tool guidance...\n# Acceptance\nReturn mismatches and exact prompt lines..." },
56
+ { id: "AuthOwner", role: "Auth Storage Reviewer", assignment: "# Target\npackages/ai/src/auth-storage.ts\n# Change\nTrace credential selection…\n# Acceptance\nReturn confirmed findings only" },
57
+ { id: "PromptOwner", role: "Prompt Contract Reviewer", assignment: "# Target\npackages/coding-agent/src/prompts/**\n# Change\nCheck active-tool guidance…\n# Acceptance\nReturn mismatches and exact prompt lines" },
58
58
  ]
59
59
  )
60
60
  {{else}}
61
61
  task(
62
62
  role: "Auth Storage Reviewer",
63
- assignment: "# Target\npackages/ai/src/auth-storage.ts\n# Change\nReview the auth diff. Shared contract: read-only; return findings as severity/file/line/fix.\n# Acceptance\nReturn confirmed findings only..."
63
+ assignment: "# Target\npackages/ai/src/auth-storage.ts\n# Change\nReview the auth diff. Shared contract: read-only; return findings as severity/file/line/fix.\n# Acceptance\nReturn confirmed findings only"
64
64
  )
65
65
  task(
66
66
  role: "Prompt Contract Reviewer",
67
- assignment: "# Target\npackages/coding-agent/src/prompts/**\n# Change\nCheck active-tool guidance. Shared contract: read-only; return mismatches and exact prompt lines.\n# Acceptance\nReturn confirmed findings only..."
67
+ assignment: "# Target\npackages/coding-agent/src/prompts/**\n# Change\nCheck active-tool guidance. Shared contract: read-only; return mismatches and exact prompt lines.\n# Acceptance\nReturn confirmed findings only"
68
68
  )
69
69
  {{/if}}
70
70
 
@@ -15,7 +15,6 @@ import librarianMd from "../prompts/agents/librarian.md" with { type: "text" };
15
15
  import planMd from "../prompts/agents/plan.md" with { type: "text" };
16
16
  import reviewerMd from "../prompts/agents/reviewer.md" with { type: "text" };
17
17
  import taskMd from "../prompts/agents/task.md" with { type: "text" };
18
- import testerMd from "../prompts/agents/tester.md" with { type: "text" };
19
18
 
20
19
  import type { AgentDefinition, AgentSource } from "./types";
21
20
 
@@ -47,7 +46,6 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
47
46
  { fileName: "designer.md", template: designerMd },
48
47
  { fileName: "reviewer.md", template: reviewerMd },
49
48
  { fileName: "librarian.md", template: librarianMd },
50
- { fileName: "tester.md", template: testerMd },
51
49
  {
52
50
  fileName: "task.md",
53
51
  frontmatter: {
@@ -1,14 +1,16 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
 
3
- // Single-entry memo for the proseOnly formatting path. During a streaming tick
4
- // the same growing thinking text is formatted up to three times (reveal count,
5
- // reveal slice, component render); this collapses them to one computation. The
6
- // `proseOnly === false` branch is a passthrough and never consults the cache, so
7
- // the key can be the text alone. A single entry is enough for the common case of
8
- // one active thinking block and never regresses (a miss recomputes exactly as
9
- // before).
10
- let formatCacheKey = "";
11
- let formatCacheValue = "";
3
+ // Single-slot-per-mode memo for formatThinkingForDisplay. During a streaming
4
+ // tick the same growing thinking text is formatted up to three times (reveal
5
+ // count, reveal slice, component render); this collapses them to one
6
+ // computation. Prose and raw modes produce different output for the same text,
7
+ // so each mode keeps its own slot. One entry per mode is enough for the common
8
+ // case of one active thinking block and never regresses (a miss recomputes
9
+ // exactly as before).
10
+ let proseCacheKey = "";
11
+ let proseCacheValue = "";
12
+ let rawCacheKey = "";
13
+ let rawCacheValue = "";
12
14
 
13
15
  export function canonicalizeMessage(text: string | null | undefined): string {
14
16
  if (!text) return "";
@@ -22,9 +24,35 @@ export function canonicalizeMessage(text: string | null | undefined): string {
22
24
  return "";
23
25
  }
24
26
 
27
+ // gpt-5.x reasoning summaries pad every summary part with an empty HTML
28
+ // comment (`**Headline**\n\n<!-- -->`), streamed as a `<!--` delta followed by
29
+ // ` -->`. Comments with actual content are left untouched.
30
+ const EMPTY_COMMENT_RE = /^<!--\s*-->$/;
31
+ const OPEN_COMMENT_RE = /^<!--\s*$/;
32
+
33
+ /**
34
+ * Whether `line` is reasoning-summary comment noise: an empty HTML comment,
35
+ * or its still-unterminated `<!--` prefix on the last line while streaming.
36
+ */
37
+ function isCommentNoise(line: string, isLastLine: boolean): boolean {
38
+ const trimmed = line.trim();
39
+ return EMPTY_COMMENT_RE.test(trimmed) || (isLastLine && OPEN_COMMENT_RE.test(trimmed));
40
+ }
41
+
42
+ /**
43
+ * Thinking text prepared for display. Both modes drop empty `<!-- -->`
44
+ * sentinel lines outside code fences (see {@link isCommentNoise}); prose-only
45
+ * mode additionally elides fenced code down to a trailing ellipsis.
46
+ */
25
47
  export function formatThinkingForDisplay(text: string, proseOnly: boolean): string {
26
- if (!proseOnly || !text) return text;
27
- if (text === formatCacheKey) return formatCacheValue;
48
+ if (!text) return text;
49
+ const hasComment = text.includes("<!--");
50
+ if (proseOnly) {
51
+ if (text === proseCacheKey) return proseCacheValue;
52
+ } else {
53
+ if (!hasComment) return text;
54
+ if (text === rawCacheKey) return rawCacheValue;
55
+ }
28
56
 
29
57
  const lines = text.split("\n");
30
58
  const resultLines: string[] = [];
@@ -56,22 +84,31 @@ export function formatThinkingForDisplay(text: string, proseOnly: boolean): stri
56
84
 
57
85
  for (let i = 0; i < lines.length; i++) {
58
86
  const line = lines[i]!;
59
- const open = FENCE.exec(line);
60
87
 
61
88
  if (inFence) {
89
+ const close = FENCE.exec(line);
62
90
  // A closing fence is the same char, at least as long, with nothing else on the line.
63
91
  if (
64
- open &&
65
- open[2]![0] === fenceChar &&
66
- open[2]!.length >= fenceLen &&
67
- line.slice(open[1]!.length + open[2]!.length).trim() === ""
92
+ close &&
93
+ close[2]![0] === fenceChar &&
94
+ close[2]!.length >= fenceLen &&
95
+ line.slice(close[1]!.length + close[2]!.length).trim() === ""
68
96
  ) {
69
97
  inFence = false;
70
98
  fenceChar = "";
71
99
  fenceLen = 0;
72
100
  }
73
- // We skip all internal lines of a code fence.
74
- } else if (open) {
101
+ // Prose mode skips all fence lines; raw mode keeps them verbatim
102
+ // (comment markers inside fences are code, not noise).
103
+ if (!proseOnly) resultLines.push(line);
104
+ continue;
105
+ }
106
+
107
+ // Drop the whole line so `**Headline**\n\n<!-- -->` leaves no blank tail.
108
+ if (hasComment && isCommentNoise(line, i === lines.length - 1)) continue;
109
+
110
+ const open = FENCE.exec(line);
111
+ if (open) {
75
112
  const marker = open[2]!;
76
113
  const ch = marker[0]!;
77
114
  // A backtick fence's info string may not contain a backtick.
@@ -79,18 +116,25 @@ export function formatThinkingForDisplay(text: string, proseOnly: boolean): stri
79
116
  inFence = true;
80
117
  fenceChar = ch;
81
118
  fenceLen = marker.length;
82
- appendEllipsis();
83
- } else {
84
- resultLines.push(line);
119
+ if (proseOnly) {
120
+ appendEllipsis();
121
+ } else {
122
+ resultLines.push(line);
123
+ }
124
+ continue;
85
125
  }
86
- } else {
87
- resultLines.push(line);
88
126
  }
127
+ resultLines.push(line);
89
128
  }
90
129
 
91
130
  const formatted = resultLines.join("\n");
92
- formatCacheKey = text;
93
- formatCacheValue = formatted;
131
+ if (proseOnly) {
132
+ proseCacheKey = text;
133
+ proseCacheValue = formatted;
134
+ } else {
135
+ rawCacheKey = text;
136
+ rawCacheValue = formatted;
137
+ }
94
138
  return formatted;
95
139
  }
96
140
 
@@ -99,9 +143,11 @@ export function hasDisplayableThinking(
99
143
  text: string | null | undefined,
100
144
  formattedText: string | null | undefined,
101
145
  ): boolean {
102
- if (!text) return false;
103
- if (!formattedText) return false;
104
- return formattedText.length > 0 && canonicalizeMessage(text).length > 0;
146
+ if (!text || !formattedText) return false;
147
+ // Visibility keys off the formatted text: a block whose raw text is only
148
+ // comment noise (`<!-- -->\n`) formats to whitespace and stays hidden. The
149
+ // raw canonicalize check still hides dot/ellipsis-only placeholder blocks.
150
+ return formattedText.trim().length > 0 && canonicalizeMessage(text).length > 0;
105
151
  }
106
152
 
107
153
  /** Whether an assistant message contains thinking content the TUI can reveal. */
@@ -1,111 +0,0 @@
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
- - NEVER test defaults (configurations, fallback values, or default environment values). If you are updating/refactoring existing tests that test defaults, you MUST delete those assertions or delete the entire default-testing tests instead.
26
- - You are explicitly ALLOWED to write **no tests at all** if you were spawned for a stupid reason (meaning: the change is trivial—such as docs, comments, types, exports, or simple config; the behavior is already fully covered; or any tests you would write would be worthless, restate plumbing, or test defaults). If so, state this clearly and exit.
27
- </critical>
28
-
29
- <anti-patterns name="worthless-tests">
30
- NEVER write any of these. Each is a green check that survives real bugs:
31
- - **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.
32
- - **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.
33
- - **Tautologies.** `expect(true).toBe(true)`, `expect(x).toBe(x)`, asserting a constant equals its literal.
34
- - **Bare no-throw.** `expect(() => f()).not.toThrow()` with no assertion on the result. "It ran" is not a contract.
35
- - **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.
36
- - **Mock round-trips.** Asserting a mock was called with the args you just passed it. You tested the mock, not the system.
37
- - **Existence/shape-only.** Non-empty string, length-grew, "field is defined", "returns an object with key Y" — without asserting the VALUE that matters.
38
- - **Default values.** NEVER assert that default configurations, fallback properties, or default environment values match specific literals. A harmless change to a default setting must never break the tests. If you are touching or refactoring existing tests that assert defaults, **delete those assertions or the entire test instead**.
39
- - **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.
40
- - **Duplicate-layer coverage.** Re-proving through mocks what an integration test already proves. Drop the narrower restatement.
41
-
42
- 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.
43
- </anti-patterns>
44
-
45
- <what-to-test>
46
- Aim every test at something that can actually break:
47
- - **Behavior & outputs** — given input, the observable result (return value, emitted event, written file, error surfaced).
48
- - **State transitions** — the legal and illegal moves of a stateful component; one test per invariant or transition, not one per field touched.
49
- - **Invariants across fields** — relationships that MUST hold (sorted output stays sorted, sum of parts equals total, encode∘decode is identity).
50
- - **Edge & boundary values** — zero, empty, one, max, negative, off-by-one, overflow, unicode, the value just inside and just outside a limit.
51
- - **Precedence & resolution** — arg beats env beats default; later override wins; first-match-wins.
52
- - **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.
53
- - **Regression-prone parsing boundaries** — the exact bytes where a parser/serializer historically broke; pin past regressions with a named case.
54
- </what-to-test>
55
-
56
- <techniques>
57
- Reach for the right shape; do not reinvent what the repo's framework already gives you.
58
- - **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.
59
- - **Subtests.** Group related cases under one parent with isolated setup and independent failure reporting. Prefer over many tiny near-duplicate test functions.
60
- - **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.
61
- - **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.
62
- - **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.
63
- - **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.
64
- - **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.
65
- </techniques>
66
-
67
- <black-box>
68
- - **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.
69
- - **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.
70
- - 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.
71
- </black-box>
72
-
73
- <fakes>
74
- - **Prefer real implementations.** If the dependency is cheap and deterministic, use the real thing.
75
- - **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.
76
- - **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.
77
- - NEVER use module-registry mocking that leaks across test files. Spy on the imported object and restore in teardown.
78
- </fakes>
79
-
80
- <isolation>
81
- Tests MUST be full-suite safe and order-independent, not merely file-local safe.
82
- - **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.
83
- - **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.
84
- - **Deterministic.** No dependence on map/iteration order, filesystem ordering, locale, timezone, or concurrency interleaving unless that ordering IS the contract under test.
85
- - **Hermetic.** No real network or real time. Each test creates and tears down its own fixtures.
86
- </isolation>
87
-
88
- <workflow>
89
- 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.
90
- 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.
91
- 3. **Enumerate contracts.** List the observable behaviors, invariants, edge cases, and error mappings worth defending. Drop anything that fails the `<critical>` litmus.
92
- 4. **Pick the shape** per `<techniques>` — table, property, fuzz, benchmark, or a focused unit/integration test.
93
- 5. **Write the tests**, matching repo conventions exactly. Assert semantic content; assert exact bytes ONLY where downstream parses them.
94
- 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.
95
- </workflow>
96
-
97
- <verify>
98
- - You MUST run the tests you wrote with the project's test command and confirm they pass.
99
- - 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.
100
- - Run ONLY the tests you added or touched unless asked for the full suite.
101
- - Report each test by the contract it defends — not "added N tests", but "covers <behavior/invariant/edge>".
102
- </verify>
103
-
104
- <critical>
105
- - A test exists to FAIL on a real bug. No nameable contract, or no plausible bug would redden it → NEVER write it.
106
- - NEVER assert plumbing, restate the implementation, or grep the source. Test observable behavior through the public surface.
107
- - No timing races, no environment pollution, deterministic and order-independent — full-suite safe.
108
- - NEVER test defaults. If updating tests that do, delete them instead.
109
- - You are explicitly ALLOWED to write **no tests at all** if you were spawned for a stupid reason (trivial changes, already covered, or if any possible test would be worthless/test defaults).
110
- - You MUST keep going until the tests are written, passing, and proven to have teeth (unless skipped per above).
111
- </critical>