@oh-my-pi/pi-coding-agent 16.0.0 → 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.
- package/CHANGELOG.md +115 -133
- package/dist/cli.js +158 -130
- package/dist/types/config/settings-schema.d.ts +22 -0
- package/dist/types/discovery/helpers.d.ts +7 -0
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
- package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
- package/dist/types/modes/types.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/session/session-manager.d.ts +4 -1
- package/dist/types/task/index.d.ts +21 -0
- package/dist/types/tools/github-cache.d.ts +5 -4
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/web/search/index.d.ts +2 -2
- package/dist/types/web/search/provider.d.ts +2 -0
- package/package.json +12 -12
- package/src/cli/args.ts +1 -0
- package/src/collab/host.ts +1 -1
- package/src/config/settings-schema.ts +23 -1
- package/src/discovery/claude-plugins.ts +3 -42
- package/src/discovery/github.ts +101 -6
- package/src/discovery/helpers.ts +11 -0
- package/src/eval/__tests__/prelude-agent.test.ts +73 -0
- package/src/eval/js/shared/prelude.txt +12 -3
- package/src/eval/py/prelude.py +26 -2
- package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
- package/src/extensibility/plugins/loader.ts +3 -2
- package/src/extensibility/plugins/manager.ts +4 -3
- package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
- package/src/extensibility/plugins/runtime-config.ts +9 -0
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/issue-pr-protocol.ts +8 -4
- package/src/main.ts +5 -1
- package/src/modes/acp/acp-agent.ts +3 -3
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +7 -0
- package/src/modes/interactive-mode.ts +47 -0
- package/src/modes/rpc/rpc-mode.ts +3 -3
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/types.ts +5 -0
- package/src/prompts/agents/designer.md +8 -0
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +4 -1
- package/src/prompts/tools/eval.md +13 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/sdk.ts +9 -1
- package/src/session/agent-session.ts +125 -18
- package/src/session/session-manager.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +5 -2
- package/src/task/executor.ts +5 -4
- package/src/task/index.ts +70 -9
- package/src/tools/github-cache.ts +32 -7
- package/src/tools/job.ts +14 -1
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +14 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SearchProviderId } from "../web/search/types";
|
|
1
2
|
/** Unified settings schema - single source of truth for all settings.
|
|
2
3
|
*
|
|
3
4
|
* Each setting is defined once here with:
|
|
@@ -3605,6 +3606,17 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
3605
3606
|
readonly description: "Enable plan mode for read-only exploration and planning before execution";
|
|
3606
3607
|
};
|
|
3607
3608
|
};
|
|
3609
|
+
readonly "plan.defaultOnStartup": {
|
|
3610
|
+
readonly type: "boolean";
|
|
3611
|
+
readonly default: false;
|
|
3612
|
+
readonly ui: {
|
|
3613
|
+
readonly tab: "tasks";
|
|
3614
|
+
readonly group: "Modes";
|
|
3615
|
+
readonly label: "Start in Plan Mode";
|
|
3616
|
+
readonly description: "Automatically enter plan mode at the start of every new session";
|
|
3617
|
+
readonly condition: "planModeEnabled";
|
|
3618
|
+
};
|
|
3619
|
+
};
|
|
3608
3620
|
readonly "goal.enabled": {
|
|
3609
3621
|
readonly type: "boolean";
|
|
3610
3622
|
readonly default: true;
|
|
@@ -4120,6 +4132,16 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
4120
4132
|
}];
|
|
4121
4133
|
};
|
|
4122
4134
|
};
|
|
4135
|
+
readonly "providers.webSearchExclude": {
|
|
4136
|
+
readonly type: "array";
|
|
4137
|
+
readonly default: SearchProviderId[];
|
|
4138
|
+
readonly ui: {
|
|
4139
|
+
readonly tab: "providers";
|
|
4140
|
+
readonly group: "Services";
|
|
4141
|
+
readonly label: "Excluded Web Search Providers";
|
|
4142
|
+
readonly description: "Providers that web_search should never use, even as fallbacks";
|
|
4143
|
+
};
|
|
4144
|
+
};
|
|
4123
4145
|
readonly "providers.image": {
|
|
4124
4146
|
readonly type: "enum";
|
|
4125
4147
|
readonly values: readonly ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"];
|
|
@@ -67,6 +67,13 @@ export declare function getUserPath(ctx: LoadContext, source: SourceId, subpath:
|
|
|
67
67
|
* Get project-level path for a source (cwd only).
|
|
68
68
|
*/
|
|
69
69
|
export declare function getProjectPath(ctx: LoadContext, source: SourceId, subpath: string): string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve GitHub Copilot CLI's user-global config root. Copilot stores per-user
|
|
72
|
+
* instructions/prompts/agents/MCP under `~/.copilot`, relocatable via the
|
|
73
|
+
* `COPILOT_HOME` env var (mirrors Copilot CLI's `--config-dir`). Falls back to
|
|
74
|
+
* `<home>/.copilot` when the override is unset.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveCopilotHome(home: string): string;
|
|
70
77
|
/**
|
|
71
78
|
* Create source metadata for an item.
|
|
72
79
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -45,6 +45,11 @@ export type SubmittedUserInput = {
|
|
|
45
45
|
* as a hidden agent-authored `developer` message rather than a visible user
|
|
46
46
|
* turn. Used by the `c`/`.` continue shortcut. */
|
|
47
47
|
synthetic?: boolean;
|
|
48
|
+
/** Marks this submission as a deliberate user resume (set by the `.`/`c`
|
|
49
|
+
* continue shortcut, which is also `synthetic`). Forwarded to
|
|
50
|
+
* `session.prompt({ userInitiated })` so it clears advisor auto-resume
|
|
51
|
+
* suppression even though it is synthetic. */
|
|
52
|
+
userInitiated?: boolean;
|
|
48
53
|
display?: boolean;
|
|
49
54
|
/** Queue intent if the session is (or becomes) busy when this submission is
|
|
50
55
|
* dispatched: "steer" (interrupt the active turn) or "followUp" (process after
|
|
@@ -250,6 +250,12 @@ export interface PromptOptions {
|
|
|
250
250
|
toolChoice?: ToolChoice;
|
|
251
251
|
/** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
|
|
252
252
|
synthetic?: boolean;
|
|
253
|
+
/** Marks this prompt as a deliberate user action (typed message, `.`/`c`
|
|
254
|
+
* continue). Clears advisor auto-resume suppression that a user interrupt set.
|
|
255
|
+
* Defaults to `!synthetic`; manual-continue is synthetic yet user-initiated, so
|
|
256
|
+
* it sets this explicitly. Agent-initiated synthetic prompts (auto-continue,
|
|
257
|
+
* plan re-prime, reminders) leave it unset and keep suppression latched. */
|
|
258
|
+
userInitiated?: boolean;
|
|
253
259
|
/** Explicit billing/initiator attribution for the prompt. Defaults to user prompts as `user` and synthetic prompts as `agent`. */
|
|
254
260
|
attribution?: MessageAttribution;
|
|
255
261
|
/** Skip pre-send compaction checks for this prompt (internal use for maintenance flows). */
|
|
@@ -1129,7 +1135,7 @@ export declare class AgentSession {
|
|
|
1129
1135
|
compact?: boolean;
|
|
1130
1136
|
}): string;
|
|
1131
1137
|
/**
|
|
1132
|
-
* Enable or disable the advisor for this session. The setting is
|
|
1138
|
+
* Enable or disable the advisor for this session. The setting is overridden for the session,
|
|
1133
1139
|
* and the runtime is started or stopped to match.
|
|
1134
1140
|
*
|
|
1135
1141
|
* @returns true when the advisor is actively running after the call.
|
|
@@ -1141,6 +1147,10 @@ export declare class AgentSession {
|
|
|
1141
1147
|
* @returns true when the advisor is actively running after the call.
|
|
1142
1148
|
*/
|
|
1143
1149
|
toggleAdvisorEnabled(): boolean;
|
|
1150
|
+
/**
|
|
1151
|
+
* Whether the advisor setting is enabled for this session.
|
|
1152
|
+
*/
|
|
1153
|
+
isAdvisorEnabled(): boolean;
|
|
1144
1154
|
/**
|
|
1145
1155
|
* Whether a live advisor agent is attached to this session. True only when
|
|
1146
1156
|
* `advisor.enabled` is set AND a model resolved for the `advisor` role AND
|
|
@@ -250,8 +250,11 @@ export declare class SessionManager {
|
|
|
250
250
|
/**
|
|
251
251
|
* Open a specific session file.
|
|
252
252
|
* @param sessionDir Optional dir for /new or /branch; defaults to the file's parent.
|
|
253
|
+
* @param options.initialCwd Cwd to use when the file is empty or missing.
|
|
253
254
|
*/
|
|
254
|
-
static open(filePath: string, sessionDir?: string, storage?: SessionStorage
|
|
255
|
+
static open(filePath: string, sessionDir?: string, storage?: SessionStorage, options?: {
|
|
256
|
+
initialCwd?: string;
|
|
257
|
+
}): Promise<SessionManager>;
|
|
255
258
|
/** Continue the most recent session, or create a new one if none exists. */
|
|
256
259
|
static continueRecent(cwd: string, sessionDir?: string, storage?: SessionStorage): Promise<SessionManager>;
|
|
257
260
|
/** Create an in-memory session (no file persistence). */
|
|
@@ -26,6 +26,27 @@ export declare function formatResultOutputFallback(result: Pick<SingleResult, "o
|
|
|
26
26
|
* the same agent ≥2× all without roles. Returns undefined when no nudge applies.
|
|
27
27
|
*/
|
|
28
28
|
export declare function buildSpecializationAdvisory(agentName: string | undefined, items: TaskItem[], depthCapacity: boolean): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Suggestion — never a rejection — nudging the spawner to coordinate via `irc`
|
|
31
|
+
* when one call creates ≥2 live siblings and it still holds spawn capacity.
|
|
32
|
+
* Returns undefined when there is nothing to coordinate or IRC is unavailable.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildCoordinationAdvisory(items: TaskItem[], depthCapacity: boolean, ircEnabled: boolean): string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Compose the non-blocking advisory appended to a `task` result: the
|
|
37
|
+
* specialization nudge, plus — only when the siblings keep running after this
|
|
38
|
+
* call (`willRunAsync`) — the coordination suggestion. Coordination is gated on
|
|
39
|
+
* async because a sync fanout's siblings have already finished, so a
|
|
40
|
+
* "coordinate while they run" hint would misfire. Returns undefined when
|
|
41
|
+
* neither applies.
|
|
42
|
+
*/
|
|
43
|
+
export declare function composeSpawnAdvisory(args: {
|
|
44
|
+
agentName: string | undefined;
|
|
45
|
+
items: TaskItem[];
|
|
46
|
+
depthCapacity: boolean;
|
|
47
|
+
ircEnabled: boolean;
|
|
48
|
+
willRunAsync: boolean;
|
|
49
|
+
}): string | undefined;
|
|
29
50
|
/**
|
|
30
51
|
* Task tool - Delegate tasks to specialized agents.
|
|
31
52
|
*
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* helpers swallow open/IO failures and degrade to "no cache" so a corrupt or
|
|
9
9
|
* unreadable DB never blocks a `gh` call.
|
|
10
10
|
*
|
|
11
|
-
* TTL:
|
|
12
11
|
* Soft TTL → return cached row directly.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Stateful issue/PR rows past soft TTL but within hard TTL → refresh
|
|
13
|
+
* synchronously, falling back to the cached row if the live fetch fails.
|
|
14
|
+
* Expensive PR diff rows past soft TTL but within hard TTL → return cached
|
|
15
|
+
* row AND schedule a background refresh (errors logged, never thrown).
|
|
15
16
|
* Past hard TTL → treat as miss and fetch fresh.
|
|
16
17
|
*/
|
|
17
18
|
import { Database } from "bun:sqlite";
|
|
@@ -101,7 +102,7 @@ export interface CacheLookupOptions<T> {
|
|
|
101
102
|
settings?: Settings | undefined;
|
|
102
103
|
now?: number;
|
|
103
104
|
}
|
|
104
|
-
export type CacheStatus = "miss" | "fresh" | "stale" | "disabled";
|
|
105
|
+
export type CacheStatus = "miss" | "fresh" | "refreshed" | "stale" | "disabled";
|
|
105
106
|
export interface CacheLookupResult<T> {
|
|
106
107
|
rendered: string;
|
|
107
108
|
sourceUrl: string | undefined;
|
|
@@ -80,6 +80,6 @@ export declare class WebSearchTool implements AgentTool<typeof webSearchSchema,
|
|
|
80
80
|
/** Web search tool as CustomTool (for TUI rendering support) */
|
|
81
81
|
export declare const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRenderDetails>;
|
|
82
82
|
export declare function getSearchTools(): CustomTool<any, any>[];
|
|
83
|
-
export { getSearchProvider, setPreferredSearchProvider } from "./provider";
|
|
83
|
+
export { getSearchProvider, setExcludedSearchProviders, setPreferredSearchProvider } from "./provider";
|
|
84
84
|
export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
|
|
85
|
-
export { isSearchProviderPreference } from "./types";
|
|
85
|
+
export { isSearchProviderId, isSearchProviderPreference } from "./types";
|
|
@@ -13,6 +13,8 @@ export declare function getSearchProviderLabel(id: SearchProviderId): string;
|
|
|
13
13
|
export declare function getSearchProvider(id: SearchProviderId): Promise<SearchProvider>;
|
|
14
14
|
/** Set the preferred web search provider from settings */
|
|
15
15
|
export declare function setPreferredSearchProvider(provider: SearchProviderId | "auto"): void;
|
|
16
|
+
/** Set providers that web search should never use, including fallbacks. */
|
|
17
|
+
export declare function setExcludedSearchProviders(providers: readonly SearchProviderId[]): void;
|
|
16
18
|
/**
|
|
17
19
|
* Determine which providers are configured and currently available.
|
|
18
20
|
* Each candidate is loaded (and its `isAvailable()` called) only as the chain
|
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.0.
|
|
4
|
+
"version": "16.0.1",
|
|
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",
|
|
@@ -47,17 +47,17 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
48
48
|
"@babel/parser": "^7.29.7",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/hashline": "16.0.
|
|
51
|
-
"@oh-my-pi/omp-stats": "16.0.
|
|
52
|
-
"@oh-my-pi/pi-agent-core": "16.0.
|
|
53
|
-
"@oh-my-pi/pi-ai": "16.0.
|
|
54
|
-
"@oh-my-pi/pi-catalog": "16.0.
|
|
55
|
-
"@oh-my-pi/pi-mnemopi": "16.0.
|
|
56
|
-
"@oh-my-pi/pi-natives": "16.0.
|
|
57
|
-
"@oh-my-pi/pi-tui": "16.0.
|
|
58
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
59
|
-
"@oh-my-pi/pi-wire": "16.0.
|
|
60
|
-
"@oh-my-pi/snapcompact": "16.0.
|
|
50
|
+
"@oh-my-pi/hashline": "16.0.1",
|
|
51
|
+
"@oh-my-pi/omp-stats": "16.0.1",
|
|
52
|
+
"@oh-my-pi/pi-agent-core": "16.0.1",
|
|
53
|
+
"@oh-my-pi/pi-ai": "16.0.1",
|
|
54
|
+
"@oh-my-pi/pi-catalog": "16.0.1",
|
|
55
|
+
"@oh-my-pi/pi-mnemopi": "16.0.1",
|
|
56
|
+
"@oh-my-pi/pi-natives": "16.0.1",
|
|
57
|
+
"@oh-my-pi/pi-tui": "16.0.1",
|
|
58
|
+
"@oh-my-pi/pi-utils": "16.0.1",
|
|
59
|
+
"@oh-my-pi/pi-wire": "16.0.1",
|
|
60
|
+
"@oh-my-pi/snapcompact": "16.0.1",
|
|
61
61
|
"@opentelemetry/api": "^1.9.1",
|
|
62
62
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
63
63
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
package/src/cli/args.ts
CHANGED
|
@@ -279,6 +279,7 @@ export function getExtraHelpText(): string {
|
|
|
279
279
|
KILO_API_KEY - Kilo Gateway models
|
|
280
280
|
MISTRAL_API_KEY - Mistral models
|
|
281
281
|
ZAI_API_KEY - z.ai models (ZhipuAI/GLM)
|
|
282
|
+
UMANS_AI_CODING_PLAN_API_KEY - Umans AI Coding Plan models
|
|
282
283
|
MINIMAX_API_KEY - MiniMax models
|
|
283
284
|
OPENCODE_API_KEY - OpenCode Zen/OpenCode Go models
|
|
284
285
|
CURSOR_ACCESS_TOKEN - Cursor AI models
|
package/src/collab/host.ts
CHANGED
|
@@ -396,7 +396,7 @@ export class CollabHost {
|
|
|
396
396
|
}
|
|
397
397
|
const name = peer.name;
|
|
398
398
|
void this.#ctx.session
|
|
399
|
-
.abort()
|
|
399
|
+
.abort({ reason: USER_INTERRUPT_LABEL })
|
|
400
400
|
.then(() => this.#ctx.session.emitNotice("info", `${name} interrupted`, "collab"))
|
|
401
401
|
.catch(err => logger.warn("collab guest abort failed", { error: String(err) }));
|
|
402
402
|
}
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
TTS_LOCAL_VOICE_VALUES,
|
|
35
35
|
} from "../tts/models";
|
|
36
36
|
import { EDIT_MODES } from "../utils/edit-mode";
|
|
37
|
-
import { SEARCH_PROVIDER_OPTIONS, SEARCH_PROVIDER_PREFERENCES } from "../web/search/types";
|
|
37
|
+
import { SEARCH_PROVIDER_OPTIONS, SEARCH_PROVIDER_PREFERENCES, type SearchProviderId } from "../web/search/types";
|
|
38
38
|
|
|
39
39
|
/** Unified settings schema - single source of truth for all settings.
|
|
40
40
|
*
|
|
@@ -3454,6 +3454,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
3454
3454
|
},
|
|
3455
3455
|
},
|
|
3456
3456
|
|
|
3457
|
+
"plan.defaultOnStartup": {
|
|
3458
|
+
type: "boolean",
|
|
3459
|
+
default: false,
|
|
3460
|
+
ui: {
|
|
3461
|
+
tab: "tasks",
|
|
3462
|
+
group: "Modes",
|
|
3463
|
+
label: "Start in Plan Mode",
|
|
3464
|
+
description: "Automatically enter plan mode at the start of every new session",
|
|
3465
|
+
condition: "planModeEnabled",
|
|
3466
|
+
},
|
|
3467
|
+
},
|
|
3468
|
+
|
|
3457
3469
|
"goal.enabled": {
|
|
3458
3470
|
type: "boolean",
|
|
3459
3471
|
default: true,
|
|
@@ -3846,6 +3858,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
3846
3858
|
options: SEARCH_PROVIDER_OPTIONS,
|
|
3847
3859
|
},
|
|
3848
3860
|
},
|
|
3861
|
+
"providers.webSearchExclude": {
|
|
3862
|
+
type: "array",
|
|
3863
|
+
default: [] as SearchProviderId[],
|
|
3864
|
+
ui: {
|
|
3865
|
+
tab: "providers",
|
|
3866
|
+
group: "Services",
|
|
3867
|
+
label: "Excluded Web Search Providers",
|
|
3868
|
+
description: "Providers that web_search should never use, even as fallbacks",
|
|
3869
|
+
},
|
|
3870
|
+
},
|
|
3849
3871
|
"providers.image": {
|
|
3850
3872
|
type: "enum",
|
|
3851
3873
|
values: ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"] as const,
|
|
@@ -124,43 +124,6 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
124
124
|
}
|
|
125
125
|
return { items, warnings };
|
|
126
126
|
}
|
|
127
|
-
async function loadSkillSlashCommands(ctx: LoadContext, root: ClaudePluginRoot): Promise<LoadResult<SlashCommand>> {
|
|
128
|
-
const { dir: skillsDir, warning } = await resolvePluginDir(root, ["skills"], "skills");
|
|
129
|
-
const warnings: string[] = warning ? [warning] : [];
|
|
130
|
-
const skillsResult = await scanSkillsFromDir(ctx, {
|
|
131
|
-
dir: skillsDir,
|
|
132
|
-
providerId: PROVIDER_ID,
|
|
133
|
-
level: root.scope,
|
|
134
|
-
});
|
|
135
|
-
warnings.push(...(skillsResult.warnings ?? []));
|
|
136
|
-
|
|
137
|
-
const commands = await Promise.all(
|
|
138
|
-
skillsResult.items.map(async skill => {
|
|
139
|
-
const content = await readFile(skill.path);
|
|
140
|
-
if (content === null) {
|
|
141
|
-
warnings.push(`Failed to read skill slash command: ${skill.path}`);
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
// Slash command name MUST come from the skill directory basename, not
|
|
145
|
-
// frontmatter `name`: `expandSlashCommand` splits the command at the first
|
|
146
|
-
// whitespace, so a display name like "Understand Anything" would never match
|
|
147
|
-
// `/understand`. The documented layout is `skills/<name>/SKILL.md` → `/<name>`.
|
|
148
|
-
const command: SlashCommand = {
|
|
149
|
-
name: path.basename(path.dirname(skill.path)),
|
|
150
|
-
path: skill.path,
|
|
151
|
-
content,
|
|
152
|
-
level: skill.level,
|
|
153
|
-
_source: skill._source,
|
|
154
|
-
};
|
|
155
|
-
return command;
|
|
156
|
-
}),
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
items: commands.filter((command): command is SlashCommand => command !== null),
|
|
161
|
-
warnings,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
127
|
|
|
165
128
|
// =============================================================================
|
|
166
129
|
// Slash Commands
|
|
@@ -189,16 +152,14 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
|
|
|
189
152
|
};
|
|
190
153
|
},
|
|
191
154
|
});
|
|
192
|
-
|
|
193
|
-
return { commandResult, skillCommandResult, warning };
|
|
155
|
+
return { commandResult, warning };
|
|
194
156
|
}),
|
|
195
157
|
);
|
|
196
158
|
|
|
197
|
-
for (const { commandResult,
|
|
159
|
+
for (const { commandResult, warning } of results) {
|
|
198
160
|
if (warning) warnings.push(warning);
|
|
199
|
-
items.push(...commandResult.items
|
|
161
|
+
items.push(...commandResult.items);
|
|
200
162
|
if (commandResult.warnings) warnings.push(...commandResult.warnings);
|
|
201
|
-
if (skillCommandResult.warnings) warnings.push(...skillCommandResult.warnings);
|
|
202
163
|
}
|
|
203
164
|
|
|
204
165
|
return { items, warnings };
|
package/src/discovery/github.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* Priority: 30 (shared standard provider)
|
|
6
6
|
*
|
|
7
7
|
* Sources:
|
|
8
|
-
* - Project: .github/ (
|
|
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
|
|
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 {
|
|
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:
|
|
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/
|
|
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
|
+
});
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|