@oh-my-pi/pi-coding-agent 15.13.3 → 16.0.1

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 (93) hide show
  1. package/CHANGELOG.md +155 -133
  2. package/dist/cli.js +621 -530
  3. package/dist/types/advisor/__tests__/advisor.test.d.ts +1 -0
  4. package/dist/types/advisor/advise-tool.d.ts +58 -0
  5. package/dist/types/advisor/index.d.ts +3 -0
  6. package/dist/types/advisor/runtime.d.ts +52 -0
  7. package/dist/types/advisor/watchdog.d.ts +5 -0
  8. package/dist/types/config/model-roles.d.ts +1 -1
  9. package/dist/types/config/settings-schema.d.ts +66 -5
  10. package/dist/types/discovery/helpers.d.ts +7 -0
  11. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
  12. package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
  13. package/dist/types/modes/components/advisor-message.d.ts +9 -0
  14. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  15. package/dist/types/modes/controllers/command-controller.d.ts +3 -1
  16. package/dist/types/modes/interactive-mode.d.ts +3 -1
  17. package/dist/types/modes/types.d.ts +8 -1
  18. package/dist/types/sdk.d.ts +3 -3
  19. package/dist/types/session/agent-session.d.ts +81 -2
  20. package/dist/types/session/session-history-format.d.ts +4 -0
  21. package/dist/types/session/session-manager.d.ts +4 -1
  22. package/dist/types/session/yield-queue.d.ts +2 -0
  23. package/dist/types/task/index.d.ts +21 -0
  24. package/dist/types/tools/github-cache.d.ts +5 -4
  25. package/dist/types/tools/job.d.ts +1 -0
  26. package/dist/types/tools/path-utils.d.ts +1 -0
  27. package/dist/types/tools/report-tool-issue.d.ts +0 -1
  28. package/dist/types/web/search/index.d.ts +2 -2
  29. package/dist/types/web/search/provider.d.ts +2 -0
  30. package/package.json +13 -13
  31. package/src/advisor/__tests__/advisor.test.ts +586 -0
  32. package/src/advisor/advise-tool.ts +87 -0
  33. package/src/advisor/index.ts +3 -0
  34. package/src/advisor/runtime.ts +248 -0
  35. package/src/advisor/watchdog.ts +83 -0
  36. package/src/cli/args.ts +1 -0
  37. package/src/collab/host.ts +1 -1
  38. package/src/config/model-roles.ts +13 -1
  39. package/src/config/settings-schema.ts +65 -6
  40. package/src/discovery/claude-plugins.ts +3 -42
  41. package/src/discovery/github.ts +101 -6
  42. package/src/discovery/helpers.ts +11 -0
  43. package/src/eval/__tests__/prelude-agent.test.ts +73 -0
  44. package/src/eval/js/shared/prelude.txt +12 -3
  45. package/src/eval/py/prelude.py +26 -2
  46. package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
  47. package/src/extensibility/plugins/loader.ts +3 -2
  48. package/src/extensibility/plugins/manager.ts +4 -3
  49. package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
  50. package/src/extensibility/plugins/runtime-config.ts +9 -0
  51. package/src/internal-urls/docs-index.generated.ts +10 -9
  52. package/src/internal-urls/issue-pr-protocol.ts +8 -4
  53. package/src/main.ts +9 -1
  54. package/src/modes/acp/acp-agent.ts +3 -3
  55. package/src/modes/components/advisor-message.ts +99 -0
  56. package/src/modes/components/agent-hub.ts +7 -0
  57. package/src/modes/components/assistant-message.ts +86 -0
  58. package/src/modes/components/settings-defs.ts +7 -0
  59. package/src/modes/components/status-line/segments.ts +20 -7
  60. package/src/modes/components/tips.txt +1 -1
  61. package/src/modes/controllers/command-controller.ts +69 -2
  62. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  63. package/src/modes/controllers/input-controller.ts +1 -0
  64. package/src/modes/controllers/selector-controller.ts +7 -0
  65. package/src/modes/interactive-mode.ts +59 -2
  66. package/src/modes/rpc/rpc-mode.ts +3 -3
  67. package/src/modes/runtime-init.ts +2 -1
  68. package/src/modes/types.ts +8 -1
  69. package/src/modes/utils/ui-helpers.ts +9 -0
  70. package/src/prompts/advisor/advise-tool.md +1 -0
  71. package/src/prompts/advisor/system.md +31 -0
  72. package/src/prompts/agents/designer.md +8 -0
  73. package/src/prompts/review-request.md +1 -1
  74. package/src/prompts/system/subagent-system-prompt.md +4 -1
  75. package/src/prompts/tools/eval.md +13 -3
  76. package/src/prompts/tools/irc.md +1 -1
  77. package/src/sdk.ts +61 -14
  78. package/src/session/agent-session.ts +667 -13
  79. package/src/session/session-dump-format.ts +15 -131
  80. package/src/session/session-history-format.ts +30 -11
  81. package/src/session/session-manager.ts +3 -1
  82. package/src/session/yield-queue.ts +5 -1
  83. package/src/slash-commands/builtin-registry.ts +105 -4
  84. package/src/system-prompt.ts +1 -1
  85. package/src/task/executor.ts +5 -4
  86. package/src/task/index.ts +70 -9
  87. package/src/tools/github-cache.ts +32 -7
  88. package/src/tools/job.ts +14 -1
  89. package/src/tools/path-utils.ts +33 -2
  90. package/src/tools/report-tool-issue.ts +2 -7
  91. package/src/web/scrapers/docs-rs.ts +2 -3
  92. package/src/web/search/index.ts +2 -2
  93. package/src/web/search/provider.ts +14 -2
@@ -5,11 +5,14 @@
5
5
  * Priority: 30 (shared standard provider)
6
6
  *
7
7
  * Sources:
8
- * - Project: .github/ (project-only, no user-level discovery)
8
+ * - Project: .github/ (repo-local Copilot config)
9
+ * - User: ~/.copilot/ (user-global Copilot CLI config; relocatable via COPILOT_HOME)
10
+ * - Extra: directories listed in COPILOT_CUSTOM_INSTRUCTIONS_DIRS
9
11
  *
10
12
  * Capabilities:
11
- * - context-files: copilot-instructions.md in .github/
12
- * - instructions: *.instructions.md in .github/instructions/ with applyTo frontmatter
13
+ * - context-files: copilot-instructions.md in .github/ and ~/.copilot/; AGENTS.md in each COPILOT_CUSTOM_INSTRUCTIONS_DIRS
14
+ * - instructions: *.instructions.md under .github/instructions/ (project) and <dir>/.github/instructions/ for each custom dir (applyTo frontmatter)
15
+ * - prompts: *.prompt.md in .github/prompts/ (VS Code Copilot prompt files)
13
16
  * - skills: <name>/SKILL.md in .github/skills/ (GitHub Agent Skills layout)
14
17
  */
15
18
  import * as path from "node:path";
@@ -18,10 +21,19 @@ import { registerProvider } from "../capability";
18
21
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
19
22
  import { readFile } from "../capability/fs";
20
23
  import { type Instruction, instructionCapability } from "../capability/instruction";
24
+ import { type Prompt, promptCapability } from "../capability/prompt";
21
25
  import { type Skill, skillCapability } from "../capability/skill";
22
26
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
23
27
 
24
- import { calculateDepth, createSourceMeta, getProjectPath, loadFilesFromDir, scanSkillsFromDir } from "./helpers";
28
+ import {
29
+ calculateDepth,
30
+ createSourceMeta,
31
+ getProjectPath,
32
+ loadFilesFromDir,
33
+ parseCSV,
34
+ resolveCopilotHome,
35
+ scanSkillsFromDir,
36
+ } from "./helpers";
25
37
 
26
38
  const PROVIDER_ID = "github";
27
39
  const DISPLAY_NAME = "GitHub Copilot";
@@ -52,6 +64,33 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
52
64
  }
53
65
  }
54
66
 
67
+ // User-global instructions (~/.copilot/copilot-instructions.md), applied across all repos.
68
+ const userInstructionsPath = path.join(resolveCopilotHome(ctx.home), "copilot-instructions.md");
69
+ const userContent = await readFile(userInstructionsPath);
70
+ if (userContent) {
71
+ items.push({
72
+ path: userInstructionsPath,
73
+ content: userContent,
74
+ level: "user",
75
+ _source: createSourceMeta(PROVIDER_ID, userInstructionsPath, "user"),
76
+ });
77
+ }
78
+
79
+ // Each COPILOT_CUSTOM_INSTRUCTIONS_DIRS entry contributes an AGENTS.md (Copilot CLI
80
+ // searches these dirs for AGENTS.md + .github/instructions/**; the latter is handled
81
+ // by loadInstructions). copilot-instructions.md is NOT part of the custom-dir spec.
82
+ for (const dir of copilotCustomInstructionDirs()) {
83
+ const agentsMdPath = path.join(dir, "AGENTS.md");
84
+ const agentsMdContent = await readFile(agentsMdPath);
85
+ if (agentsMdContent) {
86
+ items.push({
87
+ path: agentsMdPath,
88
+ content: agentsMdContent,
89
+ level: "user",
90
+ _source: createSourceMeta(PROVIDER_ID, agentsMdPath, "user"),
91
+ });
92
+ }
93
+ }
55
94
  return { items, warnings };
56
95
  }
57
96
 
@@ -65,9 +104,23 @@ async function loadInstructions(ctx: LoadContext): Promise<LoadResult<Instructio
65
104
 
66
105
  const instructionsDir = getProjectPath(ctx, "github", "instructions");
67
106
  if (instructionsDir) {
107
+ // Path-specific instructions live "within or below" .github/instructions/ → recurse.
68
108
  const result = await loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, "project", {
69
109
  extensions: ["md"],
70
110
  transform: transformInstruction,
111
+ recursive: true,
112
+ });
113
+ items.push(...result.items);
114
+ if (result.warnings) warnings.push(...result.warnings);
115
+ }
116
+
117
+ // Each COPILOT_CUSTOM_INSTRUCTIONS_DIRS entry contributes <dir>/.github/instructions/**/*.instructions.md.
118
+ for (const dir of copilotCustomInstructionDirs()) {
119
+ const customInstructionsDir = path.join(dir, ".github", "instructions");
120
+ const result = await loadFilesFromDir<Instruction>(ctx, customInstructionsDir, PROVIDER_ID, "user", {
121
+ extensions: ["md"],
122
+ transform: transformInstruction,
123
+ recursive: true,
71
124
  });
72
125
  items.push(...result.items);
73
126
  if (result.warnings) warnings.push(...result.warnings);
@@ -99,6 +152,39 @@ function transformInstruction(name: string, content: string, filePath: string, s
99
152
  };
100
153
  }
101
154
 
155
+ // =============================================================================
156
+ // Prompts
157
+ // =============================================================================
158
+
159
+ async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
160
+ // `.github/prompts/*.prompt.md` is the VS Code Copilot prompt-file convention (the
161
+ // Copilot CLI has no prompt-file feature of its own); surface them as slash commands.
162
+ const promptsDir = getProjectPath(ctx, "github", "prompts");
163
+ if (!promptsDir) return { items: [], warnings: [] };
164
+
165
+ return loadFilesFromDir<Prompt>(ctx, promptsDir, PROVIDER_ID, "project", {
166
+ extensions: ["md"],
167
+ transform: transformPrompt,
168
+ });
169
+ }
170
+
171
+ function transformPrompt(name: string, content: string, filePath: string, source: SourceMeta): Prompt | null {
172
+ // Prompt files are `*.prompt.md`; ignore other markdown that may share the dir.
173
+ if (!name.endsWith(".prompt.md")) return null;
174
+
175
+ const { frontmatter, body } = parseFrontmatter(content, { source: filePath });
176
+ const promptName =
177
+ typeof frontmatter.name === "string" && frontmatter.name ? frontmatter.name : path.basename(name, ".prompt.md");
178
+
179
+ return { name: promptName, path: filePath, content: body, _source: source };
180
+ }
181
+
182
+ /** Directories listed in the COPILOT_CUSTOM_INSTRUCTIONS_DIRS env var (comma-separated). */
183
+ function copilotCustomInstructionDirs(): string[] {
184
+ const raw = process.env.COPILOT_CUSTOM_INSTRUCTIONS_DIRS;
185
+ return raw ? parseCSV(raw) : [];
186
+ }
187
+
102
188
  // =============================================================================
103
189
  // Skills
104
190
  // =============================================================================
@@ -132,7 +218,8 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
132
218
  registerProvider(contextFileCapability.id, {
133
219
  id: PROVIDER_ID,
134
220
  displayName: DISPLAY_NAME,
135
- description: "Load copilot-instructions.md from .github/",
221
+ description:
222
+ "Load copilot-instructions.md from .github/ and ~/.copilot/; AGENTS.md from COPILOT_CUSTOM_INSTRUCTIONS_DIRS",
136
223
  priority: PRIORITY,
137
224
  load: loadContextFiles,
138
225
  });
@@ -140,7 +227,7 @@ registerProvider(contextFileCapability.id, {
140
227
  registerProvider(instructionCapability.id, {
141
228
  id: PROVIDER_ID,
142
229
  displayName: DISPLAY_NAME,
143
- description: "Load *.instructions.md from .github/instructions/ with applyTo frontmatter",
230
+ description: "Load *.instructions.md from .github/instructions/ and COPILOT_CUSTOM_INSTRUCTIONS_DIRS",
144
231
  priority: PRIORITY,
145
232
  load: loadInstructions,
146
233
  });
@@ -152,3 +239,11 @@ registerProvider<Skill>(skillCapability.id, {
152
239
  priority: PRIORITY,
153
240
  load: loadSkills,
154
241
  });
242
+
243
+ registerProvider<Prompt>(promptCapability.id, {
244
+ id: PROVIDER_ID,
245
+ displayName: DISPLAY_NAME,
246
+ description: "Load *.prompt.md from .github/prompts/ (VS Code Copilot prompt files)",
247
+ priority: PRIORITY,
248
+ load: loadPrompts,
249
+ });
@@ -107,6 +107,17 @@ export function getProjectPath(ctx: LoadContext, source: SourceId, subpath: stri
107
107
  return path.join(ctx.cwd, paths.projectDir, subpath);
108
108
  }
109
109
 
110
+ /**
111
+ * Resolve GitHub Copilot CLI's user-global config root. Copilot stores per-user
112
+ * instructions/prompts/agents/MCP under `~/.copilot`, relocatable via the
113
+ * `COPILOT_HOME` env var (mirrors Copilot CLI's `--config-dir`). Falls back to
114
+ * `<home>/.copilot` when the override is unset.
115
+ */
116
+ export function resolveCopilotHome(home: string): string {
117
+ const override = process.env.COPILOT_HOME?.trim();
118
+ return override ? override : path.join(home, ".copilot");
119
+ }
120
+
110
121
  /**
111
122
  * Create source metadata for an item.
112
123
  */
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import * as vm from "node:vm";
3
+ import { JAVASCRIPT_PRELUDE_SOURCE } from "../js/shared/prelude";
4
+
5
+ /**
6
+ * The eval `agent()` helper grows a `returnHandle` option that turns its bare
7
+ * text result into a DAG node dict carrying the spawned agent's recoverable
8
+ * `agent://<id>` handle, so a downstream `pipeline`/`parallel` stage can wire
9
+ * the transcript by reference instead of re-inlining it. These lock the node
10
+ * shape, backward compatibility of the default path, the schema interaction,
11
+ * and the no-`details` fallback (the helper must never throw).
12
+ *
13
+ * The prelude source is executed verbatim in a throwaway VM context with only
14
+ * the host bridge (`__omp_call_tool__`) stubbed — no worker, no kernel — so the
15
+ * test runs against the real shipped helper, not a re-implementation.
16
+ */
17
+ function loadPrelude(callTool: (name: string, args: unknown) => Promise<unknown>): Record<string, unknown> {
18
+ const sandbox: Record<string, unknown> = { __omp_call_tool__: callTool };
19
+ vm.createContext(sandbox);
20
+ vm.runInContext(JAVASCRIPT_PRELUDE_SOURCE, sandbox);
21
+ return sandbox;
22
+ }
23
+
24
+ type AgentHelper = (prompt: string, opts?: Record<string, unknown>) => Promise<unknown>;
25
+
26
+ describe("eval js agent() returnHandle", () => {
27
+ it("returns a DAG node carrying the agent:// handle when returnHandle is set", async () => {
28
+ let seenName: string | undefined;
29
+ const sandbox = loadPrelude(async name => {
30
+ seenName = name;
31
+ return { text: "hello world", details: { agent: "task", id: "abc123", model: "m", structured: false } };
32
+ });
33
+ const node = await (sandbox.agent as AgentHelper)("say hi", { returnHandle: true });
34
+ expect(seenName).toBe("__agent__");
35
+ expect(node).toEqual({
36
+ text: "hello world",
37
+ output: "hello world",
38
+ handle: "agent://abc123",
39
+ id: "abc123",
40
+ agent: "task",
41
+ });
42
+ });
43
+
44
+ it("returns bare text by default (backward compatible)", async () => {
45
+ const sandbox = loadPrelude(async () => ({
46
+ text: "hello world",
47
+ details: { agent: "task", id: "abc123", structured: false },
48
+ }));
49
+ const out = await (sandbox.agent as AgentHelper)("say hi");
50
+ expect(out).toBe("hello world");
51
+ });
52
+
53
+ it("carries the parsed object under data when schema and returnHandle combine", async () => {
54
+ const payload = JSON.stringify({ k: 1 });
55
+ const sandbox = loadPrelude(async () => ({
56
+ text: payload,
57
+ details: { agent: "task", id: "id-9", structured: true },
58
+ }));
59
+ const node = (await (sandbox.agent as AgentHelper)("emit", {
60
+ schema: { type: "object" },
61
+ returnHandle: true,
62
+ })) as Record<string, unknown>;
63
+ expect(node.handle).toBe("agent://id-9");
64
+ expect(node.data).toEqual({ k: 1 });
65
+ expect(node.text).toBe(payload);
66
+ });
67
+
68
+ it("falls back to a null handle without throwing when the bridge omits details", async () => {
69
+ const sandbox = loadPrelude(async () => ({ text: "lonely" }));
70
+ const node = await (sandbox.agent as AgentHelper)("x", { returnHandle: true });
71
+ expect(node).toEqual({ text: "lonely", output: "lonely", handle: null, id: null, agent: null });
72
+ });
73
+ });
@@ -117,10 +117,19 @@ if (!globalThis.__omp_js_prelude_loaded__) {
117
117
  };
118
118
 
119
119
  const agent = async (prompt, opts, ...rest) => {
120
- const o = optionsArg("agent", opts, rest, ["agentType", "model", "label", "schema"], "{ agentType, model, label, schema }");
121
- const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...o });
120
+ const o = optionsArg("agent", opts, rest, ["agentType", "model", "label", "schema"], "{ agentType, model, label, schema, returnHandle }");
121
+ const { returnHandle, ...callArgs } = o;
122
+ const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...callArgs });
122
123
  const text = res && typeof res === "object" ? res.text : res;
123
- return hasOwn(o, "schema") ? JSON.parse(text) : text;
124
+ const parsed = hasOwn(callArgs, "schema") ? JSON.parse(text) : text;
125
+ if (!returnHandle) return parsed;
126
+ const details = res && typeof res === "object" ? res.details : undefined;
127
+ if (!details || typeof details !== "object" || details.id == null) {
128
+ return { text, output: text, handle: null, id: null, agent: null };
129
+ }
130
+ const node = { text, output: text, handle: `agent://${details.id}`, id: details.id, agent: details.agent ?? null };
131
+ if (hasOwn(callArgs, "schema")) node.data = parsed;
132
+ return node;
124
133
  };
125
134
 
126
135
  // Pool ceiling mirrors the task tool's `task.maxConcurrency` setting so an
@@ -519,7 +519,7 @@ if "__omp_prelude_loaded__" not in globals():
519
519
  text = res.get("text") if isinstance(res, dict) else res
520
520
  return json.loads(text) if schema is not None else text
521
521
 
522
- def agent(prompt, *, agent_type="task", model=None, label=None, schema=None):
522
+ def agent(prompt, *, agent_type="task", model=None, label=None, schema=None, return_handle=False):
523
523
  """Run a subagent and return its final output.
524
524
 
525
525
  `agent_type` selects the subagent definition (default "task"). Pass
@@ -527,6 +527,15 @@ if "__omp_prelude_loaded__" not in globals():
527
527
  id, and `schema` to request structured JSON output; when `schema` is
528
528
  supplied the parsed object is returned. Share background by writing a
529
529
  local:// file and referencing it in the prompt.
530
+
531
+ Set `return_handle=True` to receive a DAG node dict instead of bare
532
+ text: ``{"text", "output", "handle", "id", "agent"}`` where ``handle``
533
+ is the spawned agent's recoverable ``agent://<id>`` URI. A downstream
534
+ ``pipeline``/``parallel`` stage embeds that ``handle`` (or ``output``)
535
+ in its prompt so a large transcript flows through the graph by
536
+ reference, never re-inlined. When ``schema`` is also set the parsed
537
+ object lands under ``"data"``. If the bridge returns no recoverable id
538
+ the node still resolves with ``handle=None`` — the helper never throws.
530
539
  """
531
540
  args = {"prompt": prompt}
532
541
  if agent_type is not None:
@@ -539,7 +548,22 @@ if "__omp_prelude_loaded__" not in globals():
539
548
  args["schema"] = schema
540
549
  res = _bridge_call("__agent__", args)
541
550
  text = res.get("text") if isinstance(res, dict) else res
542
- return json.loads(text) if schema is not None else text
551
+ parsed = json.loads(text) if schema is not None else text
552
+ if not return_handle:
553
+ return parsed
554
+ details = res.get("details") if isinstance(res, dict) else None
555
+ if not isinstance(details, dict) or details.get("id") is None:
556
+ return {"text": text, "output": text, "handle": None, "id": None, "agent": None}
557
+ node = {
558
+ "text": text,
559
+ "output": text,
560
+ "handle": f"agent://{details['id']}",
561
+ "id": details["id"],
562
+ "agent": details.get("agent"),
563
+ }
564
+ if schema is not None:
565
+ node["data"] = parsed
566
+ return node
543
567
 
544
568
  def _concurrency_limit():
545
569
  """Worker-pool ceiling from the host ``task.maxConcurrency`` setting.