@oh-my-pi/pi-coding-agent 16.1.1 → 16.1.2

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 (69) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/cli.js +3314 -3338
  3. package/dist/types/cli/bench-cli.d.ts +2 -1
  4. package/dist/types/config/settings-schema.d.ts +1 -1
  5. package/dist/types/main.d.ts +2 -0
  6. package/dist/types/modes/components/assistant-message.d.ts +12 -0
  7. package/dist/types/modes/components/welcome.d.ts +1 -1
  8. package/dist/types/sdk.d.ts +19 -2
  9. package/dist/types/session/auth-broker-config.d.ts +33 -6
  10. package/dist/types/system-prompt.d.ts +5 -1
  11. package/dist/types/task/executor.d.ts +10 -0
  12. package/dist/types/tools/find.d.ts +0 -2
  13. package/dist/types/tools/search.d.ts +3 -3
  14. package/package.json +12 -12
  15. package/scripts/measure-prompt-tokens.ts +63 -0
  16. package/src/cli/bench-cli.ts +64 -3
  17. package/src/cli/startup-cwd.ts +3 -13
  18. package/src/config/settings-schema.ts +1 -1
  19. package/src/cursor.ts +1 -1
  20. package/src/debug/raw-sse-buffer.ts +31 -10
  21. package/src/eval/py/prelude.py +1 -1
  22. package/src/export/html/tool-views.generated.js +1 -1
  23. package/src/extensibility/extensions/runner.ts +8 -2
  24. package/src/internal-urls/docs-index.generated.txt +1 -1
  25. package/src/main.ts +29 -9
  26. package/src/modes/components/assistant-message.ts +86 -0
  27. package/src/modes/components/tips.txt +2 -1
  28. package/src/modes/components/welcome.ts +86 -8
  29. package/src/modes/controllers/event-controller.ts +1 -1
  30. package/src/prompts/system/personalities/default.md +8 -16
  31. package/src/prompts/system/system-prompt.md +101 -115
  32. package/src/prompts/tools/ast-edit.md +10 -12
  33. package/src/prompts/tools/ast-grep.md +14 -18
  34. package/src/prompts/tools/bash.md +19 -21
  35. package/src/prompts/tools/browser.md +24 -24
  36. package/src/prompts/tools/checkpoint.md +0 -1
  37. package/src/prompts/tools/debug.md +11 -15
  38. package/src/prompts/tools/eval.md +27 -27
  39. package/src/prompts/tools/find.md +6 -10
  40. package/src/prompts/tools/github.md +11 -15
  41. package/src/prompts/tools/goal.md +0 -7
  42. package/src/prompts/tools/inspect-image.md +0 -1
  43. package/src/prompts/tools/irc.md +15 -24
  44. package/src/prompts/tools/job.md +5 -8
  45. package/src/prompts/tools/learn.md +2 -2
  46. package/src/prompts/tools/lsp.md +27 -30
  47. package/src/prompts/tools/manage-skill.md +4 -4
  48. package/src/prompts/tools/read.md +21 -23
  49. package/src/prompts/tools/replace.md +0 -1
  50. package/src/prompts/tools/resolve.md +4 -9
  51. package/src/prompts/tools/rewind.md +1 -1
  52. package/src/prompts/tools/search.md +8 -10
  53. package/src/prompts/tools/task.md +33 -38
  54. package/src/prompts/tools/todo.md +14 -18
  55. package/src/prompts/tools/web-search.md +0 -4
  56. package/src/prompts/tools/write.md +1 -1
  57. package/src/sdk.ts +49 -102
  58. package/src/session/agent-session.ts +17 -2
  59. package/src/session/auth-broker-config.ts +36 -76
  60. package/src/session/session-history-format.ts +1 -1
  61. package/src/session/session-manager.ts +33 -6
  62. package/src/system-prompt.ts +28 -8
  63. package/src/task/executor.ts +57 -0
  64. package/src/task/index.ts +15 -1
  65. package/src/tools/browser.ts +1 -1
  66. package/src/tools/eval.ts +1 -1
  67. package/src/tools/find.ts +4 -17
  68. package/src/tools/memory-edit.ts +1 -1
  69. package/src/tools/search.ts +5 -5
package/src/main.ts CHANGED
@@ -11,6 +11,7 @@ import { EventLoopKeepalive } from "@oh-my-pi/pi-agent-core";
11
11
  import type { ImageContent } from "@oh-my-pi/pi-ai";
12
12
  import {
13
13
  $env,
14
+ directoryExists,
14
15
  getLogPath,
15
16
  getProjectDir,
16
17
  logger,
@@ -575,7 +576,11 @@ async function moveMissingCwdSessionIfNeeded(
575
576
  return { status: "declined" };
576
577
  }
577
578
 
578
- const manager = await SessionManager.open(session.path, sessionDir);
579
+ // Open anchored at the (now-missing) recorded cwd: `open` otherwise falls back
580
+ // to the launch cwd, which would make the `moveTo` below a no-op whenever the
581
+ // move target equals the current project dir. moveTo never chdirs, so the
582
+ // stale cwd is only a relocation source, not a directory we enter.
583
+ const manager = await SessionManager.open(session.path, sessionDir, undefined, { initialCwd: sourceCwd });
579
584
  await manager.moveTo(cwd, sessionDir);
580
585
  return { status: "moved", manager };
581
586
  }
@@ -751,6 +756,20 @@ function discoverAppendSystemPromptFile(): string | undefined {
751
756
  return undefined;
752
757
  }
753
758
 
759
+ /** Apply resolved CLI/discovered prompt files without bypassing system prompt templates. */
760
+ export function applyResolvedSystemPromptInputs(
761
+ options: CreateAgentSessionOptions,
762
+ resolvedSystemPrompt: string | undefined,
763
+ resolvedAppendPrompt: string | undefined,
764
+ ): void {
765
+ if (resolvedSystemPrompt) {
766
+ options.customSystemPrompt = resolvedSystemPrompt;
767
+ }
768
+ if (resolvedAppendPrompt) {
769
+ options.appendSystemPrompt = resolvedAppendPrompt;
770
+ }
771
+ }
772
+
754
773
  async function buildSessionOptions(
755
774
  parsed: Args,
756
775
  scopedModels: ScopedModel[],
@@ -875,13 +894,7 @@ async function buildSessionOptions(
875
894
  // (handled by caller before createAgentSession)
876
895
 
877
896
  // System prompt
878
- if (resolvedSystemPrompt && resolvedAppendPrompt) {
879
- options.systemPrompt = defaultPrompt => [resolvedSystemPrompt, resolvedAppendPrompt, ...defaultPrompt.slice(1)];
880
- } else if (resolvedSystemPrompt) {
881
- options.systemPrompt = defaultPrompt => [resolvedSystemPrompt, ...defaultPrompt.slice(1)];
882
- } else if (resolvedAppendPrompt) {
883
- options.systemPrompt = defaultPrompt => [...defaultPrompt, resolvedAppendPrompt];
884
- }
897
+ applyResolvedSystemPromptInputs(options, resolvedSystemPrompt, resolvedAppendPrompt);
885
898
 
886
899
  // Tools
887
900
  if (parsed.noTools) {
@@ -1141,7 +1154,14 @@ export async function runRootCommand(
1141
1154
  // Resuming a session from another project: switch the process into that
1142
1155
  // project's directory and refresh cwd-derived caches before the session is
1143
1156
  // built, so settings discovery, plugins, and capabilities all scope to it.
1144
- if (selected.cwd && normalizePathForComparison(selected.cwd) !== normalizePathForComparison(getProjectDir())) {
1157
+ // Skip the chdir when the recorded project directory is gone: `setProjectDir`
1158
+ // would throw on the missing path. `SessionManager.open` then falls back to
1159
+ // the launch cwd, so the resumed session simply stays where the user is.
1160
+ if (
1161
+ selected.cwd &&
1162
+ normalizePathForComparison(selected.cwd) !== normalizePathForComparison(getProjectDir()) &&
1163
+ (await directoryExists(selected.cwd))
1164
+ ) {
1145
1165
  // Let the original (launch-cwd) plugin-root preload settle first so its
1146
1166
  // late resolution can't clobber the re-warm we trigger below.
1147
1167
  await pluginPreloadPromise.catch(() => {});
@@ -16,6 +16,59 @@ import { type CacheInvalidation, CacheInvalidationMarkerComponent } from "./cach
16
16
  */
17
17
  const MAX_TRANSCRIPT_ERROR_LINES = 8;
18
18
 
19
+ /**
20
+ * A GFM table delimiter row (`| --- | :--: |`, with or without bounding pipes).
21
+ * The header row alone does not render a table — this delimiter is what makes
22
+ * Markdown lay one out, and a streaming table re-aligns its columns as rows
23
+ * arrive. Requires at least one column pipe so a bare thematic break (`---`)
24
+ * does not match.
25
+ */
26
+ const MARKDOWN_TABLE_DELIMITER = /^ {0,3}\|?(?:[ \t]*:?-+:?[ \t]*\|)+[ \t]*:?-*:?[ \t]*$/;
27
+
28
+ /** Opening or closing fence of a code block: ≥3 backticks/tildes plus info string. */
29
+ const CODE_FENCE_LINE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
30
+
31
+ /**
32
+ * Whether `text` currently contains reflowing Markdown whose layout is not yet
33
+ * permanent: an open ` ```mermaid ` fence (the diagram reshapes as source
34
+ * arrives) or a GFM table (columns re-align as rows arrive). Used by
35
+ * {@link AssistantMessageComponent.isTranscriptBlockCommitStable}.
36
+ *
37
+ * Fence-aware: a mermaid block is detected by its opener, and table delimiters
38
+ * inside ordinary fenced code (shell pipes, ASCII separators, doc examples) are
39
+ * ignored so a long streamed code block is never held out of native scrollback.
40
+ * A delimiter counts only directly under a pipe-bearing header row, outside any
41
+ * code fence.
42
+ */
43
+ function detectLiveReflowingMarkdown(text: string): boolean {
44
+ let fence: string | null = null;
45
+ let prevLine = "";
46
+ for (const line of text.split("\n")) {
47
+ const fenceMatch = CODE_FENCE_LINE.exec(line);
48
+ if (fence !== null) {
49
+ // Inside a code block: only a bare matching closing fence ends it.
50
+ if (
51
+ fenceMatch &&
52
+ fenceMatch[2]!.trim() === "" &&
53
+ fenceMatch[1]![0] === fence[0] &&
54
+ fenceMatch[1]!.length >= fence.length
55
+ ) {
56
+ fence = null;
57
+ }
58
+ continue;
59
+ }
60
+ if (fenceMatch) {
61
+ if (/^mermaid\b/.test(fenceMatch[2]!.trim())) return true;
62
+ fence = fenceMatch[1]!;
63
+ prevLine = "";
64
+ continue;
65
+ }
66
+ if (prevLine.includes("|") && MARKDOWN_TABLE_DELIMITER.test(line)) return true;
67
+ prevLine = line;
68
+ }
69
+ return false;
70
+ }
71
+
19
72
  /**
20
73
  * Frames for the streaming "thinking" pulse rendered in place of a hidden
21
74
  * thinking block while the model is still producing it. A single fixed-width
@@ -36,6 +89,15 @@ export class AssistantMessageComponent extends Container {
36
89
  #convertedKittyImages = new Map<string, ImageContent>();
37
90
  #kittyConversionsInFlight = new Set<string>();
38
91
  #transcriptBlockFinalized: boolean;
92
+ /**
93
+ * True while a non-finalized text item carries reflowing Markdown — a
94
+ * ` ```mermaid ` fence or a GFM table — whose layout re-flows every frame as
95
+ * source arrives (a diagram reshaping, a table re-aligning its columns), so
96
+ * no prefix is byte-stable until the message finalizes. See
97
+ * {@link isTranscriptBlockCommitStable}. Recomputed in {@link updateContent}
98
+ * ahead of the fast-path return, so it tracks every stream tick.
99
+ */
100
+ #hasLiveReflowingMarkdown = false;
39
101
  /**
40
102
  * When true, the turn-ending `Error: …` line for `stopReason === "error"` is
41
103
  * suppressed because the same error is currently shown in the pinned banner
@@ -192,6 +254,21 @@ export class AssistantMessageComponent extends Container {
192
254
  return this.#transcriptBlockFinalized;
193
255
  }
194
256
 
257
+ /**
258
+ * Whether this still-live block's scrolled-off rows may be committed to
259
+ * immutable native scrollback (the {@link TranscriptContainer} durable-
260
+ * snapshot path). Reflowing Markdown — a streaming mermaid diagram or a GFM
261
+ * table — re-lays-out its body as source arrives (the diagram reshapes, the
262
+ * table re-aligns its columns), so committing an intermediate layout strands
263
+ * a stale fragment in native scrollback that only a full repaint (Ctrl+L) can
264
+ * clear. While such content is still streaming the block therefore stays
265
+ * wholly in the repaintable live region and commits once, at its final
266
+ * layout, when the turn finalizes.
267
+ */
268
+ isTranscriptBlockCommitStable(): boolean {
269
+ return this.#transcriptBlockFinalized || !this.#hasLiveReflowingMarkdown;
270
+ }
271
+
195
272
  getTranscriptBlockVersion(): number {
196
273
  return this.#blockVersion;
197
274
  }
@@ -418,6 +495,15 @@ export class AssistantMessageComponent extends Container {
418
495
  this.#lastMessage = message;
419
496
  this.#lastUpdateTransient = opts?.transient === true;
420
497
 
498
+ // Streaming reflowing Markdown (a mermaid diagram reshaping, a GFM table
499
+ // re-aligning columns) re-lays-out its body each frame; see
500
+ // isTranscriptBlockCommitStable. Detect it from raw text — a Markdown
501
+ // parser only resolves these once the closing fence / delimiter row
502
+ // arrives, but the stale native-scrollback commits happen mid-stream.
503
+ this.#hasLiveReflowingMarkdown = message.content.some(
504
+ content => content.type === "text" && detectLiveReflowingMarkdown(content.text),
505
+ );
506
+
421
507
  // Fast path: reuse Markdown children when shape is stable during streaming
422
508
  if (this.#tryFastPathUpdate(message)) return;
423
509
 
@@ -20,4 +20,5 @@ Press ctrl+r to search your prompt history and reuse a past message
20
20
  Pair up live: `/collab` shares your session through an end-to-end encrypted relay link — a teammate runs `/join <link>` to watch tool calls stream and prompt the agent from their own omp
21
21
  Press ← ← to drill into a running or finished agent and inspect its tool calls and transcript
22
22
  Hit a Codex rate limit? `/usage reset` spends a saved reset credit to immediately restore your quota
23
- No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm|kimi|anthropic…` rolls it locally for them!
23
+ No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm|kimi|anthropic…` rolls it locally for them!
24
+ Turn on `/advisor` to attach a second model that reviews every turn and quietly injects advice [NEW]
@@ -29,16 +29,73 @@ export const WELCOME_SESSION_SLOTS = 4;
29
29
  */
30
30
  export const WELCOME_LSP_SLOTS = 4;
31
31
 
32
- export function renderWelcomeTip(tip: string, boxWidth: number): string[] {
32
+ /** Trailing marker that flags a tip as a "what's new" callout. Stripped before
33
+ * wrapping (with any preceding whitespace) and replaced by {@link NEW_TAG_TEXT}
34
+ * painted as a shimmering rainbow. Non-global so `.test` stays stateless. */
35
+ const NEW_TIP_MARKER = /\s*\[NEW\]\s*$/;
36
+
37
+ /** Visible text rendered in place of {@link NEW_TIP_MARKER}. */
38
+ const NEW_TAG_TEXT = "NEW!";
39
+
40
+ /** Milliseconds for one full hue rotation of the rainbow "NEW!" tag. */
41
+ const NEW_GLOW_PERIOD_MS = 1500;
42
+
43
+ /** Selection weight for "[NEW]" tips; ordinary tips weigh 1, so a freshly added
44
+ * affordance surfaces this many times as often. */
45
+ const NEW_TIP_WEIGHT = 4;
46
+
47
+ /** Per-tip selection weights, parallel to {@link TIPS}. */
48
+ const TIP_WEIGHTS: readonly number[] = TIPS.map(tip => (NEW_TIP_MARKER.test(tip) ? NEW_TIP_WEIGHT : 1));
49
+ const TIP_WEIGHT_TOTAL = TIP_WEIGHTS.reduce((sum, weight) => sum + weight, 0);
50
+
51
+ /** Pick a tip at random, biased toward "[NEW]" tips by {@link NEW_TIP_WEIGHT}.
52
+ * Returns "" when no tips are embedded. */
53
+ function pickWeightedTip(): string {
54
+ if (TIPS.length === 0) return "";
55
+ let r = Math.random() * TIP_WEIGHT_TOTAL;
56
+ for (let i = 0; i < TIPS.length; i++) {
57
+ r -= TIP_WEIGHTS[i] ?? 1;
58
+ if (r < 0) return TIPS[i] ?? "";
59
+ }
60
+ return TIPS[TIPS.length - 1] ?? "";
61
+ }
62
+
63
+ type ColorEncoding = "ansi-16m" | "ansi-256";
64
+
65
+ /** Paint each glyph of {@link NEW_TAG_TEXT} on a moving HSL rainbow. `phase`
66
+ * rotates the hue offset cyclically; successive renders with increasing phase
67
+ * shimmer, while a fixed phase yields a still rainbow. */
68
+ function renderNewTag(phase: number, encoding: ColorEncoding): string {
69
+ const bold = "\x1b[1m";
70
+ const reset = "\x1b[0m";
71
+ const wrapped = ((phase % 1) + 1) % 1;
72
+ const chars = [...NEW_TAG_TEXT];
73
+ let out = bold;
74
+ let prev = "";
75
+ for (let i = 0; i < chars.length; i++) {
76
+ const hue = Math.round(((i / chars.length + wrapped) % 1) * 360);
77
+ const color = Bun.color(`hsl(${hue}, 95%, 60%)`, encoding) ?? "";
78
+ if (color !== prev) {
79
+ out += color;
80
+ prev = color;
81
+ }
82
+ out += chars[i];
83
+ }
84
+ return out + reset;
85
+ }
86
+ export function renderWelcomeTip(tip: string, boxWidth: number, phase = 0): string[] {
33
87
  const label = "Tip: ";
34
88
  const labelWidth = visibleWidth(label);
35
89
  const bodyBudget = boxWidth - 1 - labelWidth; // 1 = leading indent
36
90
  if (bodyBudget < 8) return [];
37
91
 
38
- const wrappedBody = wrapTextWithAnsi(replaceTabs(tip), bodyBudget);
92
+ const isNew = NEW_TIP_MARKER.test(tip);
93
+ const body = isNew ? tip.replace(NEW_TIP_MARKER, "") : tip;
94
+
95
+ const wrappedBody = wrapTextWithAnsi(replaceTabs(body), bodyBudget);
39
96
  if (wrappedBody.length === 0) return [];
40
97
 
41
- const encoding = TERMINAL.trueColor ? "ansi-16m" : "ansi-256";
98
+ const encoding: ColorEncoding = TERMINAL.trueColor ? "ansi-16m" : "ansi-256";
42
99
  const purple = Bun.color("#b48cff", encoding) ?? "";
43
100
  const lightBlue = Bun.color("#9ccfff", encoding) ?? "";
44
101
  const italic = "\x1b[3m";
@@ -46,11 +103,27 @@ export function renderWelcomeTip(tip: string, boxWidth: number): string[] {
46
103
  const reset = "\x1b[0m";
47
104
  const continuationIndent = padding(labelWidth);
48
105
 
49
- return wrappedBody.map((body, index) =>
106
+ const lines = wrappedBody.map((line, index) =>
50
107
  index === 0
51
- ? ` ${italic}${purple}${label}${dim}${lightBlue}${body}${reset}`
52
- : ` ${italic}${continuationIndent}${dim}${lightBlue}${body}${reset}`,
108
+ ? ` ${italic}${purple}${label}${dim}${lightBlue}${line}${reset}`
109
+ : ` ${italic}${continuationIndent}${dim}${lightBlue}${line}${reset}`,
53
110
  );
111
+
112
+ if (isNew) {
113
+ // Append the rainbow tag to the final body line when it fits within the
114
+ // box; otherwise drop it onto its own indented continuation line so the
115
+ // styled glyphs never overflow or reflow the wrapped body.
116
+ const tag = renderNewTag(phase, encoding);
117
+ const tagWidth = 1 + visibleWidth(NEW_TAG_TEXT); // 1 = space separator
118
+ const lastLine = lines[lines.length - 1];
119
+ if (lastLine !== undefined && visibleWidth(lastLine) + tagWidth <= boxWidth) {
120
+ lines[lines.length - 1] = `${lastLine} ${tag}`;
121
+ } else {
122
+ lines.push(` ${continuationIndent}${tag}`);
123
+ }
124
+ }
125
+
126
+ return lines;
54
127
  }
55
128
 
56
129
  export interface RecentSession {
@@ -89,7 +162,7 @@ export class WelcomeComponent implements Component {
89
162
  if (theme.getSymbolPreset() === "unicode" && Math.random() < 0.1) {
90
163
  this.#selectedTip = "Please use nerdfont 😭.";
91
164
  } else {
92
- this.#selectedTip = TIPS.length > 0 ? TIPS[Math.floor(Math.random() * TIPS.length)] : "";
165
+ this.#selectedTip = pickWeightedTip();
93
166
  }
94
167
  }
95
168
  return this.#selectedTip || undefined;
@@ -327,7 +400,12 @@ export class WelcomeComponent implements Component {
327
400
  #renderTip(boxWidth: number): string[] {
328
401
  const tip = this.tip;
329
402
  if (!tip) return [];
330
- return renderWelcomeTip(tip, boxWidth);
403
+ // A trailing "[NEW]" marker paints an animated rainbow "NEW!" tag. Derive
404
+ // its hue phase from wall-clock time so it shimmers across the welcome
405
+ // intro's re-render frames, then settles into a still rainbow once the box
406
+ // caches its resting frame. Non-"[NEW]" tips ignore the phase entirely.
407
+ const phase = NEW_TIP_MARKER.test(tip) ? performance.now() / NEW_GLOW_PERIOD_MS : 0;
408
+ return renderWelcomeTip(tip, boxWidth, phase);
331
409
  }
332
410
 
333
411
  /** Center text within a given width */
@@ -186,7 +186,7 @@ export class EventController {
186
186
  }
187
187
  #updateWorkingMessageFromIntent(intent: unknown): void {
188
188
  if (this.ctx.session.isAborting) return;
189
- // Streamed JSON can deliver non-string `_i` (object, number, boolean) before
189
+ // Streamed JSON can deliver non-string `i` (object, number, boolean) before
190
190
  // schema validation; `?.` only guards null/undefined, so guard the type too.
191
191
  if (typeof intent !== "string") return;
192
192
  const trimmed = intent.trim();
@@ -1,26 +1,18 @@
1
1
  You are a terse, evidence-first engineer: every sentence carries a fact, a decision, or a risk.
2
2
 
3
3
  # Tone
4
- - Use terse sentence fragments when clearer.
5
- - Skip ceremony, hedging, summaries, filler, motivational and marketing language, and generic explanation.
6
- - Do not narrate obvious steps or over-explain basics.
7
- - MUST assume the reader is technical.
8
- - Be concrete: mention exact files, symbols, APIs, state fields, edge cases, and verification.
9
- - Compress reasoning into facts, constraints, tradeoffs, decisions, and checks. Action-oriented and dense.
10
- - Do not hide uncertainty: state it briefly at the specific claim, name the tradeoff, and pick the boring/safe option.
4
+ - Terse fragments when clearer. Skip ceremony, hedging, summaries, filler, and marketing language.
5
+ - Don't narrate obvious steps or over-explain basics. Assume a technical reader.
6
+ - Be concrete: exact files, symbols, APIs, state fields, edge cases, verification.
7
+ - Compress reasoning into facts, constraints, tradeoffs, decisions, checks. Lead with the conclusion, then evidence.
8
+ - Don't hide uncertainty: state it at the specific claim, name the tradeoff, pick the boring/safe option.
11
9
  - For code, focus on invariants, risks, and verification.
12
- - Lead with the conclusion, then concrete evidence: changed files and verification.
13
10
 
14
11
  # Reasoning Format
15
- - Problem: what is wrong.
16
- - Decision: what to do & why (concrete facts).
17
- - Check: what can break & how to verify result.
18
- - Next: the next concrete edit/action.
12
+ - Problem: what's wrong. Decision: what to do & why. Check: what can break & how to verify. Next: the next concrete action.
19
13
 
20
14
  # Succinct Patterns
21
- - Y → Need update X.
22
- - This is safe: Z.
23
- - Could do A, but B avoids C.
15
+ - Y → need update X. This is safe: Z. Could do A, but B avoids C.
24
16
 
25
17
  # Escalation
26
- Push back when the plan hides risk or a claim is wrong: name the risk, show the evidence, propose the alternative. Once overruled, execute the user's call without relitigating.
18
+ Push back when the plan hides risk or a claim is wrong: name the risk, show evidence, propose the alternative. Once overruled, execute the user's call without relitigating.