@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.10

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.
@@ -266,10 +266,6 @@ export class SelectorController {
266
266
  this.ctx.updateEditorBorderColor();
267
267
  break;
268
268
 
269
- case "clearOnShrink":
270
- this.ctx.ui.setClearOnShrink(value as boolean);
271
- break;
272
-
273
269
  case "autocompleteMaxVisible":
274
270
  this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
275
271
  break;
@@ -409,7 +409,6 @@ export class InteractiveMode implements InteractiveModeContext {
409
409
  }
410
410
 
411
411
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
412
- this.ui.setClearOnShrink(settings.get("clearOnShrink"));
413
412
  this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
414
413
  // OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
415
414
  // capability (`TERMINAL.textSizing` defaults on for Kitty) so it stays off
@@ -429,7 +428,7 @@ export class InteractiveMode implements InteractiveModeContext {
429
428
  this.ui.requestRender(true);
430
429
  };
431
430
  this.editor.onAutocompleteUpdate = () => {
432
- this.ui.requestRender(false, { allowUnknownViewportMutation: true });
431
+ this.ui.requestRender();
433
432
  };
434
433
  this.#syncEditorMaxHeight();
435
434
  this.#resizeHandler = () => {
@@ -959,13 +958,6 @@ export class InteractiveMode implements InteractiveModeContext {
959
958
  }
960
959
  this.editor.setText("");
961
960
  this.editor.imageLinks = undefined;
962
- // Reconciliation checkpoint: only retire frozen block snapshots after TUI
963
- // proves the native viewport is at the tail and replays scrollback safely.
964
- // Unknown host viewports stay frozen; thawing them would expose live rows
965
- // over stale native history and can yank or duplicate when ED3 is unsafe.
966
- if (this.ui.refreshNativeScrollbackIfDirty()) {
967
- this.chatContainer.thaw();
968
- }
969
961
  this.ensureLoadingAnimation();
970
962
  this.ui.requestRender();
971
963
  return submission;
@@ -2587,7 +2579,7 @@ export class InteractiveMode implements InteractiveModeContext {
2587
2579
  this.ui.requestRender(true);
2588
2580
  };
2589
2581
  nextEditor.onAutocompleteUpdate = () => {
2590
- this.ui.requestRender(false, { allowUnknownViewportMutation: true });
2582
+ this.ui.requestRender();
2591
2583
  };
2592
2584
  nextEditor.setMaxHeight(this.#computeEditorMaxHeight());
2593
2585
  if (this.historyStorage) {
@@ -60,10 +60,10 @@ You MUST use the specialized tool over its shell equivalent:
60
60
  {{#has tools "search"}}- regex search → `{{toolRefs.search}}`, not `grep`/`rg`/`awk`{{/has}}
61
61
  {{#has tools "find"}}- file globbing → `{{toolRefs.find}}`, not `ls **/*.ext`/`fd`{{/has}}
62
62
  {{#has tools "eval"}}- Then, you MAY use `{{toolRefs.eval}}` for quick compute, but you SHOULD go step by step.{{/has}}
63
- {{#has tools "bash"}}- Finally, you MAY use `{{toolRefs.bash}}` for simple one-liners only. But this is a last resort. Bash commands matching the patterns above are intercepted and blocked at runtime.
63
+ {{#has tools "bash"}}- Finally, you MAY use `{{toolRefs.bash}}` for terminal work — builds, tests, git, package managers — and for pipelines that COMPUTE a new fact: `wc -l`, `sort | uniq -c`, `comm`, `diff a b`, checksums. Commands shadowing the tools above are intercepted and blocked at runtime.
64
+ - Litmus: produces a count, frequency table, set difference, or checksum no tool returns → bash. Merely moves, pages, or trims bytes a tool can fetch → use the tool.
64
65
  - You NEVER read line ranges with `sed -n 'A,Bp'`, `awk 'NR≥A && NR≤B'`, or `head | tail` pipelines. Use `{{toolRefs.read}}` with `offset`/`limit`.
65
- - You NEVER use `2>&1` or `2>/dev/null` stdout and stderr are already merged.
66
- - You NEVER suffix commands with `| head -n N` or `| tail -n N` — the harness already streams output and returns a truncated view, with the full result available via `artifact://<id>`.
66
+ - You NEVER trim or silence output: no `| head -n N`, `| tail -n N`, `2>&1`, `2>/dev/null`. stderr is already merged; long output is auto-truncated with the full capture kept at `artifact://<id>`. Trimming destroys data the artifact would have saved.
67
67
  - If you catch yourself typing `cat`, `head`, `tail`, `less`, `more`, `ls`, `grep`, `rg`, `find`, `fd`, `sed -i`, `awk -i`, or a heredoc redirect inside a Bash call, stop and switch to the dedicated tool.{{/has}}
68
68
  {{#has tools "report_tool_issue"}}
69
69
  <critical>
@@ -13,9 +13,9 @@ Executes bash command in shell session for terminal operations like git, bun, ca
13
13
  </instruction>
14
14
 
15
15
  <critical>
16
- - NEVER use Linux coreutils (`cat`, `head`, `tail`, `less`, `more`, `ls`, `grep`, `rg`, `awk`, `sed`, `find`, `fd`, etc.) when a dedicated tool suffices ALWAYS prefer `read`, `search`, `find`, `edit`, `write`.
17
- - NEVER pipe through `| head -n N` or `| tail -n N` output is already truncated with the full result available via `artifact://<id>`.
18
- - NEVER redirect with `2>&1` or `2>/dev/null` stdout and stderr are already merged.
16
+ - NEVER use shell to fetch, display, list, page, or search content a dedicated tool serves: `cat`/`head`/`tail`/`less`/`more`/`ls` `read`; `grep`/`rg`/`ag`/`ack` `search`; `find`/`fd` `find`; `sed -i`/`perl -i`/`awk -i` `edit`; `echo >`/heredoc `write`. The tools keep gitignore semantics, line anchors, and structured output that shell loses.
17
+ - NEVER trim or silence output: no `| head -n N`, `| tail -n N`, `| less`, `2>&1`, `2>/dev/null`. stderr is already merged; long output is auto-truncated with the FULL capture kept at `artifact://<id>`. Defensive trimming is a habit from harnesses without artifact recovery — here it only destroys data the artifact would have saved.
18
+ - Pipelines that COMPUTE a new fact are correct bash: `wc -l`, `sort | uniq -c`, `comm`, `cut`, `diff a b`, `shasum`. Litmus: produces a count, frequency table, set difference, or checksum no tool returns → bash. Merely moves or trims bytes a tool can fetch → use the tool.
19
19
  </critical>
20
20
 
21
21
  <output>
@@ -2,7 +2,7 @@
2
2
 
3
3
  Manages a phased task list. Pass `ops`: a flat array of operations.
4
4
  The next pending task is auto-promoted to `in_progress` after each completion.
5
- Allowed `op` values are only `init`, `start`, `done`, `drop`, `rm`, `append`, and `note`. `pending` is a task status, not an `op`; leave not-yet-started tasks implicit in `init`/`append` lists.
5
+ Allowed `op` values are only `init`, `start`, `done`, `drop`, `rm`, `append`, `note`, and `view`. `pending` is a task status, not an `op`; leave not-yet-started tasks implicit in `init`/`append` lists.
6
6
 
7
7
  ## Operations
8
8
 
@@ -15,6 +15,7 @@ Allowed `op` values are only `init`, `start`, `done`, `drop`, `rm`, `append`, an
15
15
  |`rm`|`task` or `phase`|Remove|
16
16
  |`append`|`phase`, `items: string[]`|Append tasks to `phase`; lazily creates phase|
17
17
  |`note`|`task`, `text`|Append a note to a task. Reminders for future-you only.|
18
+ |`view`|—|Read-only: echo the current list without modifying it|
18
19
 
19
20
  ## Anatomy
20
21
  - **Task content**: 5–10 words, what is being done, not how. Used as the task identifier — unique.
@@ -25,6 +26,7 @@ Allowed `op` values are only `init`, `start`, `done`, `drop`, `rm`, `append`, an
25
26
  - Complete phases in order.
26
27
  - On blockers, `append` a new task to the active phase to unblock yourself, or `drop`.
27
28
  - `task` and `phase` fields reference content/name verbatim; keep them stable once introduced.
29
+ - Lost track of exact task text? `view` echoes the full list — NEVER guess content from memory; a mismatched `task` string is an error.
28
30
 
29
31
  ## When to create a list
30
32
  - Task requires 3+ distinct steps
@@ -35,6 +37,8 @@ Allowed `op` values are only `init`, `start`, `done`, `drop`, `rm`, `append`, an
35
37
  <examples>
36
38
  # Initial setup (multi-phase)
37
39
  `{"ops":[{"op":"init","list":[{"phase":"Foundation","items":["Scaffold crate","Wire workspace"]},{"phase":"Auth","items":["Port credential store","Wire OAuth providers"]},{"phase":"Verification","items":["Run cargo test"]}]}]}`
40
+ # View current state (read-only)
41
+ `{"ops":[{"op":"view"}]}`
38
42
  # Initial setup (single phase)
39
43
  `{"ops":[{"op":"init","list":[{"phase":"Implementation","items":["Apply fix","Run tests"]}]}]}`
40
44
  # Complete one task
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Agent discovery from filesystem.
3
3
  *
4
- * Discovers agent definitions from:
5
- * - ~/.omp/agent/agents/*.md (user-level, primary)
6
- * - ~/.pi/agent/agents/*.md (user-level, legacy)
7
- * - ~/.claude/agents/*.md (user-level, legacy)
8
- * - .omp/agents/*.md (project-level, primary)
9
- * - .pi/agents/*.md (project-level, legacy)
10
- * - .claude/agents/*.md (project-level, legacy)
4
+ * Discovers agent definitions from OMP-native task-agent roots:
5
+ * - ~/.omp/agent/agents/*.md (user-level)
6
+ * - .omp/agents/*.md (project-level)
7
+ *
8
+ * Claude Code marketplace plugin agents are discovered separately via the
9
+ * claude-plugins provider. Direct cross-harness roots such as .claude/agents
10
+ * are intentionally skipped because their frontmatter schema is not the OMP
11
+ * task-agent contract.
11
12
  *
12
13
  * Agent files use markdown with YAML frontmatter.
13
14
  */
@@ -21,6 +22,8 @@ import { listClaudePluginRoots } from "../discovery/helpers";
21
22
  import { loadBundledAgents, parseAgent } from "./agents";
22
23
  import type { AgentDefinition, AgentSource } from "./types";
23
24
 
25
+ const TASK_AGENT_CONFIG_SOURCE = ".omp";
26
+
24
27
  /** Result of agent discovery */
25
28
  export interface DiscoveryResult {
26
29
  agents: AgentDefinition[];
@@ -52,41 +55,31 @@ async function loadAgentsFromDir(dir: string, source: AgentSource): Promise<Agen
52
55
  /**
53
56
  * Discover agents from filesystem and merge with bundled agents.
54
57
  *
55
- * Precedence (highest wins): .omp > .pi > .claude (project before user), then bundled
56
- *
58
+ * Precedence (highest wins): project .omp, user .omp, Claude plugin agents, then bundled
57
59
  * @param cwd - Current working directory for project agent discovery
58
60
  */
59
61
  export async function discoverAgents(cwd: string, home: string = os.homedir()): Promise<DiscoveryResult> {
60
62
  const resolvedCwd = path.resolve(cwd);
61
- const agentSources = Array.from(new Set(getConfigDirs("", { project: false }).map(entry => entry.source)));
62
63
 
63
- // Get user directories (priority order: .omp, .pi, .claude, ...)
64
64
  const userDirs = getConfigDirs("agents", { project: false })
65
- .filter(entry => agentSources.includes(entry.source))
65
+ .filter(entry => entry.source === TASK_AGENT_CONFIG_SOURCE)
66
66
  .map(entry => ({
67
67
  ...entry,
68
68
  path: path.resolve(entry.path),
69
69
  }));
70
70
 
71
- // Get project directories by walking up from cwd (priority order)
72
71
  const projectDirs = findAllNearestProjectConfigDirs("agents", resolvedCwd)
73
- .filter(entry => agentSources.includes(entry.source))
72
+ .filter(entry => entry.source === TASK_AGENT_CONFIG_SOURCE)
74
73
  .map(entry => ({
75
74
  ...entry,
76
75
  path: path.resolve(entry.path),
77
76
  }));
78
77
 
79
- const orderedSources = agentSources.filter(
80
- source => userDirs.some(entry => entry.source === source) || projectDirs.some(entry => entry.source === source),
81
- );
82
-
83
78
  const orderedDirs: Array<{ dir: string; source: AgentSource }> = [];
84
- for (const source of orderedSources) {
85
- const project = projectDirs.find(entry => entry.source === source);
86
- if (project) orderedDirs.push({ dir: project.path, source: "project" });
87
- const user = userDirs.find(entry => entry.source === source);
88
- if (user) orderedDirs.push({ dir: user.path, source: "user" });
89
- }
79
+ const project = projectDirs[0];
80
+ if (project) orderedDirs.push({ dir: project.path, source: "project" });
81
+ const user = userDirs[0];
82
+ if (user) orderedDirs.push({ dir: user.path, source: "user" });
90
83
 
91
84
  // Load agents from Claude Code marketplace plugins (respects disabledProviders)
92
85
  const { roots: pluginRoots } = isProviderEnabled("claude-plugins")
@@ -122,7 +122,7 @@ function tinyWorkerSpawnCmd(): string[] {
122
122
  }
123
123
 
124
124
  interface SpawnedSubprocess {
125
- proc: Subprocess<"ignore", "inherit", "inherit">;
125
+ proc: Subprocess<"ignore", "ignore", "ignore">;
126
126
  inbound: Set<(message: TinyTitleWorkerOutbound) => void>;
127
127
  errors: Set<(error: Error) => void>;
128
128
  /**
@@ -147,10 +147,13 @@ export function createTinyTitleSubprocess(): SpawnedSubprocess {
147
147
  cmd: tinyWorkerSpawnCmd(),
148
148
  env: tinyWorkerEnv(),
149
149
  stdin: "ignore",
150
- stdout: "inherit",
151
- stderr: "inherit",
150
+ stdout: "ignore",
151
+ stderr: "ignore",
152
152
  serialization: "advanced",
153
153
  windowsHide: true,
154
+ // The worker is an implementation detail of the interactive TUI. Native
155
+ // model runtimes may print progress or decoded text directly; never let
156
+ // those bytes inherit the terminal and corrupt the chat scrollback.
154
157
  ipc(message) {
155
158
  for (const handler of inbound) handler(message as TinyTitleWorkerOutbound);
156
159
  },
package/src/tools/todo.ts CHANGED
@@ -51,7 +51,7 @@ export interface TodoToolDetails {
51
51
  // =============================================================================
52
52
 
53
53
  const TodoOp = z
54
- .enum(["init", "start", "done", "rm", "drop", "append", "note"] as const)
54
+ .enum(["init", "start", "done", "rm", "drop", "append", "note", "view"] as const)
55
55
  .describe("operation to apply");
56
56
 
57
57
  const InitListEntry = z.object({
@@ -380,6 +380,8 @@ function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string
380
380
  }
381
381
  case "append":
382
382
  return appendItems(phases, entry, errors);
383
+ case "view":
384
+ return phases;
383
385
  }
384
386
  }
385
387
 
@@ -523,9 +525,12 @@ export function markdownToPhases(md: string): { phases: TodoPhase[]; errors: str
523
525
  return { phases, errors };
524
526
  }
525
527
 
526
- function formatSummary(phases: TodoPhase[], errors: string[]): string {
528
+ function formatSummary(phases: TodoPhase[], errors: string[], readOnly = false): string {
527
529
  const tasks = phases.flatMap(phase => phase.tasks);
528
- if (tasks.length === 0) return errors.length > 0 ? `Errors: ${errors.join("; ")}` : "Todo list cleared.";
530
+ if (tasks.length === 0) {
531
+ if (errors.length > 0) return `Errors: ${errors.join("; ")}`;
532
+ return readOnly ? "Todo list is empty." : "Todo list cleared.";
533
+ }
529
534
 
530
535
  const remainingByPhase = phases
531
536
  .map(phase => ({
@@ -608,15 +613,19 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
608
613
  _context?: AgentToolContext,
609
614
  ): Promise<AgentToolResult<TodoToolDetails>> {
610
615
  const previousPhases = clonePhases(this.session.getTodoPhases?.() ?? []);
611
- const { phases: updated, errors } = applyParams(clonePhases(previousPhases), params);
612
- const completedTasks = getCompletionTransitions(previousPhases, updated);
613
- this.session.setTodoPhases?.(updated);
616
+ // Pure-view calls are reads: no normalization, no state write.
617
+ const readOnly = params.ops.every(entry => entry.op === "view");
618
+ const { phases: updated, errors } = readOnly
619
+ ? { phases: previousPhases, errors: [] as string[] }
620
+ : applyParams(clonePhases(previousPhases), params);
621
+ const completedTasks = readOnly ? [] : getCompletionTransitions(previousPhases, updated);
622
+ if (!readOnly) this.session.setTodoPhases?.(updated);
614
623
  const storage = this.session.getSessionFile() ? "session" : "memory";
615
624
  const details: TodoToolDetails = { phases: updated, storage };
616
625
  if (completedTasks.length > 0) details.completedTasks = completedTasks;
617
626
 
618
627
  return {
619
- content: [{ type: "text", text: formatSummary(updated, errors) }],
628
+ content: [{ type: "text", text: formatSummary(updated, errors, readOnly) }],
620
629
  details,
621
630
  isError: errors.length > 0 ? true : undefined,
622
631
  };
@@ -16,6 +16,7 @@ import {
16
16
  type FetchImpl,
17
17
  stripClaudeToolPrefix,
18
18
  withAuth,
19
+ wrapFetchForCch,
19
20
  } from "@oh-my-pi/pi-ai";
20
21
  import { $env } from "@oh-my-pi/pi-utils";
21
22
  import type {
@@ -64,7 +65,9 @@ function buildSystemBlocks(
64
65
  model: string,
65
66
  systemPrompt?: string,
66
67
  ): AnthropicSystemBlock[] | undefined {
67
- const includeClaudeCode = !model.startsWith("claude-3-5-haiku");
68
+ // Match the streaming path: the CC billing header + system instruction are
69
+ // an OAuth fingerprint and must not be claimed on API-key requests.
70
+ const includeClaudeCode = auth.isOAuth && !model.startsWith("claude-3-5-haiku");
68
71
  const extraInstructions = auth.isOAuth ? ["You are a helpful AI assistant with web search capabilities."] : [];
69
72
 
70
73
  return buildAnthropicSystemBlocks(systemPrompt ? [systemPrompt] : undefined, {
@@ -118,7 +121,10 @@ async function callSearch(
118
121
  body.system = systemBlocks;
119
122
  }
120
123
 
121
- const response = await fetchImpl(url, {
124
+ // OAuth requests inject the CC billing header (buildSystemBlocks); patch its
125
+ // cch attestation like the streaming path instead of shipping `cch=00000`.
126
+ const doFetch = auth.isOAuth ? wrapFetchForCch(fetchImpl) : fetchImpl;
127
+ const response = await doFetch(url, {
122
128
  method: "POST",
123
129
  headers,
124
130
  body: JSON.stringify(body),