@yagni-app/code 0.1.0 → 0.2.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/README.md +65 -11
- package/dist/claudeCompat.d.ts +109 -0
- package/dist/claudeCompat.js +260 -0
- package/dist/claudePlugins.d.ts +109 -0
- package/dist/claudePlugins.js +336 -0
- package/dist/cli.d.ts +1 -5
- package/dist/cli.js +79 -20
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -2
- package/dist/crashReport.d.ts +135 -0
- package/dist/crashReport.js +291 -0
- package/dist/credentials.js +2 -1
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.js +81 -3
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +194 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +87 -0
- package/dist/extension/boostCommand.d.ts +144 -0
- package/dist/extension/boostCommand.js +263 -0
- package/dist/extension/branding.d.ts +79 -0
- package/dist/extension/branding.js +156 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +373 -0
- package/dist/extension/claudeRules.d.ts +54 -0
- package/dist/extension/claudeRules.js +180 -0
- package/dist/extension/config.d.ts +173 -0
- package/dist/extension/config.js +194 -0
- package/dist/extension/costHud.d.ts +186 -0
- package/dist/extension/costHud.js +293 -0
- package/dist/extension/crashReport.d.ts +89 -0
- package/dist/extension/crashReport.js +241 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +155 -0
- package/dist/extension/index.js +597 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +170 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/mcpTools.d.ts +57 -0
- package/dist/extension/mcpTools.js +132 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +205 -0
- package/dist/extension/pipeline/eval.js +226 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +146 -0
- package/dist/extension/pipeline/goCommand.js +1085 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
- package/dist/extension/pipeline/goCompareCommand.js +203 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +645 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runCostTable.d.ts +37 -0
- package/dist/extension/pipeline/runCostTable.js +165 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +177 -0
- package/dist/extension/pipeline/runState.js +275 -0
- package/dist/extension/pipeline/runner.d.ts +98 -0
- package/dist/extension/pipeline/runner.js +310 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +53 -0
- package/dist/extension/pipeline/stages.js +115 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +439 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +763 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +202 -0
- package/dist/extension/provider.d.ts +17 -0
- package/dist/extension/provider.js +33 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +190 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/rerouteNotice.d.ts +34 -0
- package/dist/extension/rerouteNotice.js +79 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/sessionRuns.d.ts +45 -0
- package/dist/extension/sessionRuns.js +77 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +145 -0
- package/dist/extension/subagents.js +326 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +25 -3
- package/dist/launch.js +27 -9
- package/dist/login.d.ts +7 -0
- package/dist/login.js +3 -1
- package/dist/paths.d.ts +13 -4
- package/dist/paths.js +17 -5
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +5 -2
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +11 -15
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The general `subagent` tool — delegate a self-contained task to an agent
|
|
3
|
+
* with its own fresh context window, optionally in parallel.
|
|
4
|
+
*
|
|
5
|
+
* Reuses the /go pipeline's battle-tested child runner (`runStage`: NDJSON
|
|
6
|
+
* stream folding, output caps, abort escalation) through its `personaBody`
|
|
7
|
+
* seam, so a subagent is exactly a grounded pi child whose system prompt is
|
|
8
|
+
* the agent definition's body instead of a /go persona.
|
|
9
|
+
*
|
|
10
|
+
* Agent definitions are the Claude Code format — markdown with
|
|
11
|
+
* name/description/tools/model frontmatter — discovered from the repo's
|
|
12
|
+
* `.claude/agents/` and `.pi/agents/`, the user's `~/.claude/agents/`, and
|
|
13
|
+
* any Claude Code plugin `agents/` dirs the launcher passed via
|
|
14
|
+
* `YAGNI_CLAUDE_AGENT_DIRS` (project beats user beats plugin on a name
|
|
15
|
+
* collision). A built-in `general` agent is always available, so the tool
|
|
16
|
+
* works in a repo with no definitions at all.
|
|
17
|
+
* Claude model names map onto YAGNI tiers (opus→advanced, sonnet→standard,
|
|
18
|
+
* haiku→efficient); unknown models run on standard.
|
|
19
|
+
*/
|
|
20
|
+
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { Type } from "typebox";
|
|
22
|
+
import { runStage } from "./pipeline/runner.js";
|
|
23
|
+
import type { ModelTier, PipelineStage } from "./pipeline/types.js";
|
|
24
|
+
export declare const SUBAGENT_TOOL_NAME = "subagent";
|
|
25
|
+
export declare const GENERAL_AGENT_NAME = "general";
|
|
26
|
+
export declare const MAX_PARALLEL_SUBAGENTS = 4;
|
|
27
|
+
/**
|
|
28
|
+
* The default tool surface a subagent gets when its definition declares none:
|
|
29
|
+
* the full working set plus grounded answers, mirroring what a Claude Code
|
|
30
|
+
* subagent would inherit.
|
|
31
|
+
*/
|
|
32
|
+
export declare const DEFAULT_SUBAGENT_TOOLS: string[];
|
|
33
|
+
export type SubagentSource = "project-claude" | "project-pi" | "user-claude" | "plugin" | "builtin";
|
|
34
|
+
/**
|
|
35
|
+
* Claude Code plugin `agents/` dirs, delimiter-joined by the yagni launcher
|
|
36
|
+
* (see yagni-code-cli claudeCompat.ts — trust gating happens there).
|
|
37
|
+
*/
|
|
38
|
+
export declare const CLAUDE_AGENT_DIRS_ENV = "YAGNI_CLAUDE_AGENT_DIRS";
|
|
39
|
+
export interface SubagentDef {
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
tools?: string[];
|
|
43
|
+
model: ModelTier;
|
|
44
|
+
/** The agent's system prompt (the markdown body). */
|
|
45
|
+
body: string;
|
|
46
|
+
source: SubagentSource;
|
|
47
|
+
}
|
|
48
|
+
/** Claude Code model names → YAGNI tiers; YAGNI tiers pass through. */
|
|
49
|
+
export declare function mapModelTier(model: string | undefined): ModelTier;
|
|
50
|
+
/**
|
|
51
|
+
* Parse one Claude Code-format agent file. Returns null when the file has no
|
|
52
|
+
* description — the one field the tool cannot do without (the model picks
|
|
53
|
+
* agents by it).
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseAgentMarkdown(content: string, fallbackName: string, source: SubagentSource): SubagentDef | null;
|
|
56
|
+
export interface DiscoverDeps {
|
|
57
|
+
cwd: string;
|
|
58
|
+
homeDir?: string;
|
|
59
|
+
/** Env seam for the plugin agent dirs (defaults to process.env). */
|
|
60
|
+
env?: NodeJS.ProcessEnv;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* All available agents: built-in general, then Claude Code plugin `agents/`
|
|
64
|
+
* dirs (launcher-provided), then user `~/.claude/agents`, then the repo's
|
|
65
|
+
* `.pi/agents` and `.claude/agents` — later wins on a name collision, so a
|
|
66
|
+
* project definition beats a user one beats a plugin one beats the built-in.
|
|
67
|
+
*/
|
|
68
|
+
export declare function discoverSubagents(deps: DiscoverDeps): SubagentDef[];
|
|
69
|
+
export declare function formatAgentList(agents: SubagentDef[]): string;
|
|
70
|
+
/**
|
|
71
|
+
* A subagent invocation is a synthetic pipeline stage: the task rides in as
|
|
72
|
+
* `{ticket}` verbatim, and the agent's body replaces the /go persona through
|
|
73
|
+
* the runner's personaBody seam.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildSubagentStage(def: SubagentDef, task: string): {
|
|
76
|
+
stage: PipelineStage;
|
|
77
|
+
ctx: {
|
|
78
|
+
ticket: string;
|
|
79
|
+
grounded: boolean;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
type SubagentParams = {
|
|
83
|
+
task?: string;
|
|
84
|
+
agent?: string;
|
|
85
|
+
tasks?: Array<{
|
|
86
|
+
task: string;
|
|
87
|
+
agent?: string;
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
90
|
+
export interface MakeSubagentToolDeps {
|
|
91
|
+
runStageImpl?: typeof runStage;
|
|
92
|
+
discover?: (deps: DiscoverDeps) => SubagentDef[];
|
|
93
|
+
homeDir?: string;
|
|
94
|
+
}
|
|
95
|
+
export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
96
|
+
name: string;
|
|
97
|
+
label: string;
|
|
98
|
+
description: string;
|
|
99
|
+
promptSnippet: string;
|
|
100
|
+
parameters: Type.TObject<{
|
|
101
|
+
task: Type.TOptional<Type.TString>;
|
|
102
|
+
agent: Type.TOptional<Type.TString>;
|
|
103
|
+
tasks: Type.TOptional<Type.TArray<Type.TObject<{
|
|
104
|
+
task: Type.TString;
|
|
105
|
+
agent: Type.TOptional<Type.TString>;
|
|
106
|
+
}>>>;
|
|
107
|
+
}>;
|
|
108
|
+
execute(_toolCallId: string, params: SubagentParams, signal?: AbortSignal, onUpdate?: (update: {
|
|
109
|
+
content: Array<{
|
|
110
|
+
type: "text";
|
|
111
|
+
text: string;
|
|
112
|
+
}>;
|
|
113
|
+
details: unknown;
|
|
114
|
+
}) => void, ctx?: ExtensionContext): Promise<{
|
|
115
|
+
content: {
|
|
116
|
+
type: "text";
|
|
117
|
+
text: string;
|
|
118
|
+
}[];
|
|
119
|
+
details: {};
|
|
120
|
+
isError: boolean;
|
|
121
|
+
} | {
|
|
122
|
+
isError?: boolean | undefined;
|
|
123
|
+
content: {
|
|
124
|
+
type: "text";
|
|
125
|
+
text: string;
|
|
126
|
+
}[];
|
|
127
|
+
details: {
|
|
128
|
+
tasks: {
|
|
129
|
+
agent: string;
|
|
130
|
+
task: string;
|
|
131
|
+
exitCode: number;
|
|
132
|
+
usage: import("./pipeline/types.js").StageUsage;
|
|
133
|
+
toolCalls: number;
|
|
134
|
+
}[];
|
|
135
|
+
};
|
|
136
|
+
}>;
|
|
137
|
+
};
|
|
138
|
+
export interface RegisterSubagentsDeps {
|
|
139
|
+
discover?: (deps: DiscoverDeps) => SubagentDef[];
|
|
140
|
+
homeDir?: string;
|
|
141
|
+
}
|
|
142
|
+
/** Wire the subagent tool and the /agents listing command. */
|
|
143
|
+
export declare function registerSubagents(pi: ExtensionAPI, deps?: RegisterSubagentsDeps): void;
|
|
144
|
+
export {};
|
|
145
|
+
//# sourceMappingURL=subagents.d.ts.map
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The general `subagent` tool — delegate a self-contained task to an agent
|
|
3
|
+
* with its own fresh context window, optionally in parallel.
|
|
4
|
+
*
|
|
5
|
+
* Reuses the /go pipeline's battle-tested child runner (`runStage`: NDJSON
|
|
6
|
+
* stream folding, output caps, abort escalation) through its `personaBody`
|
|
7
|
+
* seam, so a subagent is exactly a grounded pi child whose system prompt is
|
|
8
|
+
* the agent definition's body instead of a /go persona.
|
|
9
|
+
*
|
|
10
|
+
* Agent definitions are the Claude Code format — markdown with
|
|
11
|
+
* name/description/tools/model frontmatter — discovered from the repo's
|
|
12
|
+
* `.claude/agents/` and `.pi/agents/`, the user's `~/.claude/agents/`, and
|
|
13
|
+
* any Claude Code plugin `agents/` dirs the launcher passed via
|
|
14
|
+
* `YAGNI_CLAUDE_AGENT_DIRS` (project beats user beats plugin on a name
|
|
15
|
+
* collision). A built-in `general` agent is always available, so the tool
|
|
16
|
+
* works in a repo with no definitions at all.
|
|
17
|
+
* Claude model names map onto YAGNI tiers (opus→advanced, sonnet→standard,
|
|
18
|
+
* haiku→efficient); unknown models run on standard.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { delimiter, join } from "node:path";
|
|
23
|
+
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { Type } from "typebox";
|
|
25
|
+
import { sanitizeCallerSegment } from "./config.js";
|
|
26
|
+
import { runStage } from "./pipeline/runner.js";
|
|
27
|
+
/**
|
|
28
|
+
* YAG-471 attribution: the `x-yagni-caller` prefix for a subagent invocation.
|
|
29
|
+
* The sanitized agent name is capped so the WHOLE label (prefix + name) stays
|
|
30
|
+
* within the model proxy's 64-char caller-label limit.
|
|
31
|
+
*/
|
|
32
|
+
const SUBAGENT_CALLER_PREFIX = "subagent:";
|
|
33
|
+
export const SUBAGENT_TOOL_NAME = "subagent";
|
|
34
|
+
export const GENERAL_AGENT_NAME = "general";
|
|
35
|
+
export const MAX_PARALLEL_SUBAGENTS = 4;
|
|
36
|
+
/**
|
|
37
|
+
* The default tool surface a subagent gets when its definition declares none:
|
|
38
|
+
* the full working set plus grounded answers, mirroring what a Claude Code
|
|
39
|
+
* subagent would inherit.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_SUBAGENT_TOOLS = [
|
|
42
|
+
"read",
|
|
43
|
+
"grep",
|
|
44
|
+
"find",
|
|
45
|
+
"ls",
|
|
46
|
+
"bash",
|
|
47
|
+
"edit",
|
|
48
|
+
"write",
|
|
49
|
+
"ask_yagni",
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Claude Code plugin `agents/` dirs, delimiter-joined by the yagni launcher
|
|
53
|
+
* (see yagni-code-cli claudeCompat.ts — trust gating happens there).
|
|
54
|
+
*/
|
|
55
|
+
export const CLAUDE_AGENT_DIRS_ENV = "YAGNI_CLAUDE_AGENT_DIRS";
|
|
56
|
+
const CLAUDE_COMPAT_DISABLE_ENV = "YAGNI_DISABLE_CLAUDE_COMPAT";
|
|
57
|
+
function pluginAgentDirs(env) {
|
|
58
|
+
const disabled = env[CLAUDE_COMPAT_DISABLE_ENV];
|
|
59
|
+
if (disabled !== undefined && disabled !== "" && disabled !== "0")
|
|
60
|
+
return [];
|
|
61
|
+
const raw = env[CLAUDE_AGENT_DIRS_ENV];
|
|
62
|
+
if (!raw)
|
|
63
|
+
return [];
|
|
64
|
+
return raw.split(delimiter).filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
const GENERAL_BODY = `You are a capable software-engineering subagent with a fresh context window. Complete the task you are given end to end, autonomously.
|
|
67
|
+
|
|
68
|
+
You are grounded in how THIS company works: call ask_yagni before inferring a convention, an ownership rule, or anything organization-specific.
|
|
69
|
+
|
|
70
|
+
Your final message is your report back to the driving agent, which has NOT seen what you read or did. Make it compressed and complete: what you did, what you found, exact file paths and key excerpts, and anything the driver must know before continuing.`;
|
|
71
|
+
const GENERAL_AGENT = {
|
|
72
|
+
name: GENERAL_AGENT_NAME,
|
|
73
|
+
description: "General-purpose agent for research, multi-file changes, and self-contained tasks.",
|
|
74
|
+
model: "standard",
|
|
75
|
+
body: GENERAL_BODY,
|
|
76
|
+
source: "builtin",
|
|
77
|
+
};
|
|
78
|
+
// Concrete tiers a subagent can actually run on. `balanced` is deliberately NOT
|
|
79
|
+
// a member here even though it is a member of `ModelTier`: a subagent needs
|
|
80
|
+
// ONE model for its whole run, and balanced is a session-level routing policy,
|
|
81
|
+
// not a rung — see mapModelTier's explicit branch below.
|
|
82
|
+
const TIER_VALUES = ["peak", "advanced", "standard", "efficient"];
|
|
83
|
+
/** Claude Code model names → YAGNI tiers; YAGNI tiers pass through. */
|
|
84
|
+
export function mapModelTier(model) {
|
|
85
|
+
if (!model)
|
|
86
|
+
return "standard";
|
|
87
|
+
const normalized = model.trim().toLowerCase();
|
|
88
|
+
if (TIER_VALUES.includes(normalized))
|
|
89
|
+
return normalized;
|
|
90
|
+
// `balanced` names a SESSION routing policy, not a tier a subagent can run
|
|
91
|
+
// on. Handled explicitly rather than falling through to the catch-all
|
|
92
|
+
// below, so frontmatter that names the policy reads as an intentional
|
|
93
|
+
// "router-decide" choice, not an unrecognized string: it lands on the
|
|
94
|
+
// built-in default lane (standard) — the session-level policy (driver
|
|
95
|
+
// turns) owns balanced's resolution; this is just a safe landing spot for
|
|
96
|
+
// frontmatter that names it.
|
|
97
|
+
if (normalized === "balanced")
|
|
98
|
+
return "standard";
|
|
99
|
+
if (normalized.includes("opus"))
|
|
100
|
+
return "advanced";
|
|
101
|
+
if (normalized.includes("sonnet"))
|
|
102
|
+
return "standard";
|
|
103
|
+
if (normalized.includes("haiku"))
|
|
104
|
+
return "efficient";
|
|
105
|
+
return "standard";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parse one Claude Code-format agent file. Returns null when the file has no
|
|
109
|
+
* description — the one field the tool cannot do without (the model picks
|
|
110
|
+
* agents by it).
|
|
111
|
+
*/
|
|
112
|
+
export function parseAgentMarkdown(content, fallbackName, source) {
|
|
113
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
114
|
+
const description = frontmatter.description?.trim();
|
|
115
|
+
if (!description)
|
|
116
|
+
return null;
|
|
117
|
+
const tools = frontmatter.tools
|
|
118
|
+
?.split(",")
|
|
119
|
+
.map((t) => t.trim())
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
return {
|
|
122
|
+
name: (frontmatter.name?.trim() || fallbackName).toLowerCase(),
|
|
123
|
+
description,
|
|
124
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
125
|
+
model: mapModelTier(frontmatter.model),
|
|
126
|
+
body: body.trim() || GENERAL_BODY,
|
|
127
|
+
source,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function loadAgentsFromDir(dir, source) {
|
|
131
|
+
let entries;
|
|
132
|
+
try {
|
|
133
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const agents = [];
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry.name.endsWith(".md"))
|
|
141
|
+
continue;
|
|
142
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
143
|
+
continue;
|
|
144
|
+
try {
|
|
145
|
+
const content = fs.readFileSync(join(dir, entry.name), "utf8");
|
|
146
|
+
const def = parseAgentMarkdown(content, entry.name.replace(/\.md$/, ""), source);
|
|
147
|
+
if (def)
|
|
148
|
+
agents.push(def);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return agents;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* All available agents: built-in general, then Claude Code plugin `agents/`
|
|
158
|
+
* dirs (launcher-provided), then user `~/.claude/agents`, then the repo's
|
|
159
|
+
* `.pi/agents` and `.claude/agents` — later wins on a name collision, so a
|
|
160
|
+
* project definition beats a user one beats a plugin one beats the built-in.
|
|
161
|
+
*/
|
|
162
|
+
export function discoverSubagents(deps) {
|
|
163
|
+
const home = deps.homeDir ?? homedir();
|
|
164
|
+
const layers = [
|
|
165
|
+
[GENERAL_AGENT],
|
|
166
|
+
...pluginAgentDirs(deps.env ?? process.env).map((dir) => loadAgentsFromDir(dir, "plugin")),
|
|
167
|
+
loadAgentsFromDir(join(home, ".claude", "agents"), "user-claude"),
|
|
168
|
+
loadAgentsFromDir(join(deps.cwd, ".pi", "agents"), "project-pi"),
|
|
169
|
+
loadAgentsFromDir(join(deps.cwd, ".claude", "agents"), "project-claude"),
|
|
170
|
+
];
|
|
171
|
+
const byName = new Map();
|
|
172
|
+
for (const layer of layers) {
|
|
173
|
+
for (const agent of layer)
|
|
174
|
+
byName.set(agent.name, agent);
|
|
175
|
+
}
|
|
176
|
+
return [...byName.values()];
|
|
177
|
+
}
|
|
178
|
+
const SOURCE_LABEL = {
|
|
179
|
+
builtin: "built-in",
|
|
180
|
+
plugin: "claude plugin",
|
|
181
|
+
"user-claude": "~/.claude/agents",
|
|
182
|
+
"project-pi": ".pi/agents",
|
|
183
|
+
"project-claude": ".claude/agents",
|
|
184
|
+
};
|
|
185
|
+
export function formatAgentList(agents) {
|
|
186
|
+
return agents
|
|
187
|
+
.map((a) => `${a.name} (${SOURCE_LABEL[a.source]}): ${a.description}`)
|
|
188
|
+
.join("\n");
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* A subagent invocation is a synthetic pipeline stage: the task rides in as
|
|
192
|
+
* `{ticket}` verbatim, and the agent's body replaces the /go persona through
|
|
193
|
+
* the runner's personaBody seam.
|
|
194
|
+
*/
|
|
195
|
+
export function buildSubagentStage(def, task) {
|
|
196
|
+
return {
|
|
197
|
+
stage: {
|
|
198
|
+
id: "implement",
|
|
199
|
+
agent: def.name,
|
|
200
|
+
model: def.model,
|
|
201
|
+
tools: def.tools ?? DEFAULT_SUBAGENT_TOOLS,
|
|
202
|
+
taskTemplate: "{ticket}",
|
|
203
|
+
},
|
|
204
|
+
ctx: { ticket: task, grounded: true },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const parameters = Type.Object({
|
|
208
|
+
task: Type.Optional(Type.String({
|
|
209
|
+
description: "The self-contained task. Include everything needed: the subagent shares none of your context.",
|
|
210
|
+
})),
|
|
211
|
+
agent: Type.Optional(Type.String({ description: "Agent name (see /agents). Defaults to general." })),
|
|
212
|
+
tasks: Type.Optional(Type.Array(Type.Object({
|
|
213
|
+
task: Type.String(),
|
|
214
|
+
agent: Type.Optional(Type.String()),
|
|
215
|
+
}), {
|
|
216
|
+
description: `Run several independent tasks in parallel (max ${MAX_PARALLEL_SUBAGENTS}). Use INSTEAD of task.`,
|
|
217
|
+
})),
|
|
218
|
+
});
|
|
219
|
+
export function makeSubagentTool(deps = {}) {
|
|
220
|
+
const run = deps.runStageImpl ?? runStage;
|
|
221
|
+
const discover = deps.discover ?? discoverSubagents;
|
|
222
|
+
return {
|
|
223
|
+
name: SUBAGENT_TOOL_NAME,
|
|
224
|
+
label: "Subagent",
|
|
225
|
+
description: "Delegate a self-contained task to a subagent with its own fresh context window, and get back " +
|
|
226
|
+
"a compressed report. Use it for context-heavy exploration you don't need blow-by-blow, or to " +
|
|
227
|
+
"run independent tasks in parallel via `tasks`. The subagent shares NONE of your conversation: " +
|
|
228
|
+
"spell out the task completely. Agents come from this repo's .claude/agents and .pi/agents " +
|
|
229
|
+
"(list them with /agents); omit `agent` for the general-purpose one.",
|
|
230
|
+
promptSnippet: "subagent: delegate a self-contained task (or parallel tasks) to a fresh-context agent; returns its report.",
|
|
231
|
+
parameters,
|
|
232
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
233
|
+
const fail = (text) => ({
|
|
234
|
+
content: [{ type: "text", text }],
|
|
235
|
+
details: {},
|
|
236
|
+
isError: true,
|
|
237
|
+
});
|
|
238
|
+
const requested = params.tasks && params.tasks.length > 0
|
|
239
|
+
? params.tasks
|
|
240
|
+
: params.task
|
|
241
|
+
? [{ task: params.task, agent: params.agent }]
|
|
242
|
+
: [];
|
|
243
|
+
if (requested.length === 0) {
|
|
244
|
+
return fail("Error: pass `task` (or a `tasks` array) describing what to do.");
|
|
245
|
+
}
|
|
246
|
+
if (requested.length > MAX_PARALLEL_SUBAGENTS) {
|
|
247
|
+
return fail(`Error: at most ${MAX_PARALLEL_SUBAGENTS} parallel tasks per call.`);
|
|
248
|
+
}
|
|
249
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
250
|
+
const agents = discover({ cwd, homeDir: deps.homeDir });
|
|
251
|
+
const resolved = [];
|
|
252
|
+
for (const req of requested) {
|
|
253
|
+
const name = (req.agent ?? GENERAL_AGENT_NAME).toLowerCase();
|
|
254
|
+
const def = agents.find((a) => a.name === name);
|
|
255
|
+
if (!def) {
|
|
256
|
+
return fail(`Error: unknown agent "${name}". Available agents:\n${formatAgentList(agents)}`);
|
|
257
|
+
}
|
|
258
|
+
resolved.push({ def, task: req.task });
|
|
259
|
+
}
|
|
260
|
+
onUpdate?.({
|
|
261
|
+
content: [
|
|
262
|
+
{
|
|
263
|
+
type: "text",
|
|
264
|
+
text: resolved.length === 1
|
|
265
|
+
? `Running ${resolved[0].def.name} subagent…`
|
|
266
|
+
: `Running ${resolved.length} subagents in parallel…`,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
details: {},
|
|
270
|
+
});
|
|
271
|
+
const outcomes = await Promise.all(resolved.map(async ({ def, task }) => {
|
|
272
|
+
const { stage, ctx: stageCtx } = buildSubagentStage(def, task);
|
|
273
|
+
const result = await run(stage, stageCtx, {
|
|
274
|
+
cwd,
|
|
275
|
+
signal,
|
|
276
|
+
personaBody: () => def.body,
|
|
277
|
+
// YAG-471: attribute this child's completions to the specific
|
|
278
|
+
// subagent, not the generic /go stage label the runner would
|
|
279
|
+
// otherwise derive from stage.id ("implement", reused as the
|
|
280
|
+
// synthetic subagent stage — see buildSubagentStage).
|
|
281
|
+
callerLabel: `${SUBAGENT_CALLER_PREFIX}${sanitizeCallerSegment(def.name, 64 - SUBAGENT_CALLER_PREFIX.length)}`,
|
|
282
|
+
});
|
|
283
|
+
return { agent: def.name, task, result };
|
|
284
|
+
}));
|
|
285
|
+
const allFailed = outcomes.every((o) => o.result.exitCode !== 0);
|
|
286
|
+
const sections = outcomes.map((o) => {
|
|
287
|
+
const output = o.result.finalOutput.trim();
|
|
288
|
+
const failedNote = o.result.exitCode !== 0
|
|
289
|
+
? `\n\n(subagent failed, exit ${o.result.exitCode}${o.result.stderr.trim() ? `: ${o.result.stderr.trim().slice(-500)}` : ""})`
|
|
290
|
+
: "";
|
|
291
|
+
const bodyText = output || (o.result.exitCode === 0 ? "(no output)" : "");
|
|
292
|
+
return outcomes.length === 1
|
|
293
|
+
? `${bodyText}${failedNote}`
|
|
294
|
+
: `## ${o.agent}: ${o.task}\n\n${bodyText}${failedNote}`;
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text", text: sections.join("\n\n") }],
|
|
298
|
+
details: {
|
|
299
|
+
tasks: outcomes.map((o) => ({
|
|
300
|
+
agent: o.agent,
|
|
301
|
+
task: o.task,
|
|
302
|
+
exitCode: o.result.exitCode,
|
|
303
|
+
usage: o.result.usage,
|
|
304
|
+
toolCalls: o.result.toolCalls,
|
|
305
|
+
})),
|
|
306
|
+
},
|
|
307
|
+
...(allFailed ? { isError: true } : {}),
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/** Wire the subagent tool and the /agents listing command. */
|
|
313
|
+
export function registerSubagents(pi, deps = {}) {
|
|
314
|
+
const discover = deps.discover ?? discoverSubagents;
|
|
315
|
+
pi.registerTool(makeSubagentTool(deps));
|
|
316
|
+
pi.registerCommand("agents", {
|
|
317
|
+
description: "List the subagents available in this repo (.claude/agents, .pi/agents).",
|
|
318
|
+
handler: async (_args, ctx) => {
|
|
319
|
+
if (!ctx.hasUI)
|
|
320
|
+
return;
|
|
321
|
+
const agents = discover({ cwd: ctx.cwd ?? process.cwd(), homeDir: deps.homeDir });
|
|
322
|
+
ctx.ui.notify(`Available subagents:\n${formatAgentList(agents)}`, "info");
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=subagents.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which surface this process is painting for. The desktop driver sets
|
|
3
|
+
* YAGNI_SURFACE=desktop; the terminal CLI sets nothing. The two renderers are
|
|
4
|
+
* mutually exclusive: a desktop session never receives ANSI, which is why the
|
|
5
|
+
* escape-code leak is fixed by construction rather than stripped downstream.
|
|
6
|
+
* Desktop-facing widgets are structured single-line JSON records the app
|
|
7
|
+
* parses, not themed terminal lines.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isDesktopSurface(): boolean;
|
|
10
|
+
//# sourceMappingURL=surface.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which surface this process is painting for. The desktop driver sets
|
|
3
|
+
* YAGNI_SURFACE=desktop; the terminal CLI sets nothing. The two renderers are
|
|
4
|
+
* mutually exclusive: a desktop session never receives ANSI, which is why the
|
|
5
|
+
* escape-code leak is fixed by construction rather than stripped downstream.
|
|
6
|
+
* Desktop-facing widgets are structured single-line JSON records the app
|
|
7
|
+
* parses, not themed terminal lines.
|
|
8
|
+
*/
|
|
9
|
+
export function isDesktopSurface() {
|
|
10
|
+
return process.env.YAGNI_SURFACE === "desktop";
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=surface.js.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session todo list — the visible checklist for multi-step work.
|
|
3
|
+
*
|
|
4
|
+
* A `todo_write` tool the model calls with the FULL list every time (replace,
|
|
5
|
+
* not patch: replacement is idempotent under retries and always renders a
|
|
6
|
+
* coherent board), a persistent above-editor widget while steps remain open,
|
|
7
|
+
* and a `/todos` command to pull the list on demand.
|
|
8
|
+
*
|
|
9
|
+
* State follows pi's branching model the same way the session does: the
|
|
10
|
+
* canonical list is the LAST `todo_write` tool result on the current branch,
|
|
11
|
+
* reconstructed on session_start/session_tree, so forking or rewinding a
|
|
12
|
+
* session automatically shows the list as it stood at that point. The
|
|
13
|
+
* in-memory copy is just a cache of that.
|
|
14
|
+
*/
|
|
15
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { Type } from "typebox";
|
|
17
|
+
export declare const TODO_TOOL_NAME = "todo_write";
|
|
18
|
+
export declare const MAX_TODOS = 50;
|
|
19
|
+
export declare const MAX_TODO_TEXT = 300;
|
|
20
|
+
/**
|
|
21
|
+
* The desktop's structured state record rides its own widget key, like the
|
|
22
|
+
* `/go` run state: one JSON line the app parses and renders itself, never
|
|
23
|
+
* themed terminal lines. The desktop shell hand-declares the matching parser
|
|
24
|
+
* (`parseTodoState` in its piProtocol.ts); a key-set assertion in
|
|
25
|
+
* todos.test.ts guards the two against drift.
|
|
26
|
+
*/
|
|
27
|
+
export declare const TODO_STATE_KEY = "yagni-todos:state";
|
|
28
|
+
export type TodoStatus = "pending" | "in_progress" | "completed";
|
|
29
|
+
export interface TodoItem {
|
|
30
|
+
text: string;
|
|
31
|
+
status: TodoStatus;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate a full replacement list. Strict: this is model input rendered
|
|
35
|
+
* straight into the terminal. An empty list is valid (it clears the board).
|
|
36
|
+
*/
|
|
37
|
+
export declare function normalizeTodos(raw: unknown): {
|
|
38
|
+
ok: true;
|
|
39
|
+
todos: TodoItem[];
|
|
40
|
+
} | {
|
|
41
|
+
ok: false;
|
|
42
|
+
error: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function todoSummary(todos: TodoItem[]): {
|
|
45
|
+
done: number;
|
|
46
|
+
total: number;
|
|
47
|
+
active: string | null;
|
|
48
|
+
};
|
|
49
|
+
/** Plain-text checklist (tool results, /todos in headless contexts). */
|
|
50
|
+
export declare function formatTodoList(todos: TodoItem[]): string;
|
|
51
|
+
/** The slice of pi's Theme the widget styles with (matches the feed's pattern). */
|
|
52
|
+
export interface TodoTheme {
|
|
53
|
+
fg(color: string, s: string): string;
|
|
54
|
+
strikethrough?(s: string): string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The above-editor checklist. Empty (paint nothing) when there is no list or
|
|
58
|
+
* every step is completed — a finished board should leave the screen. Capped
|
|
59
|
+
* so header + items + overflow stays under pi's 10-line widget truncation.
|
|
60
|
+
*/
|
|
61
|
+
export declare function renderTodoWidget(todos: TodoItem[], theme: TodoTheme): string[];
|
|
62
|
+
/** The desktop state record: exactly one JSON line under TODO_STATE_KEY. */
|
|
63
|
+
export declare function todoStateLine(todos: TodoItem[]): string;
|
|
64
|
+
/** Replay the branch: the last todo_write result is the canonical list. */
|
|
65
|
+
export declare function reconstructTodos(entries: unknown[]): TodoItem[];
|
|
66
|
+
type TodoParams = {
|
|
67
|
+
todos: Array<{
|
|
68
|
+
text: string;
|
|
69
|
+
status: string;
|
|
70
|
+
}>;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Build the todo_write tool definition around a shared store. Separated from
|
|
74
|
+
* registration so tests can drive execute directly.
|
|
75
|
+
*/
|
|
76
|
+
export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoItem[]) => void): {
|
|
77
|
+
name: string;
|
|
78
|
+
label: string;
|
|
79
|
+
description: string;
|
|
80
|
+
promptSnippet: string;
|
|
81
|
+
parameters: Type.TObject<{
|
|
82
|
+
todos: Type.TArray<Type.TObject<{
|
|
83
|
+
text: Type.TString;
|
|
84
|
+
status: Type.TUnion<[Type.TLiteral<"pending">, Type.TLiteral<"in_progress">, Type.TLiteral<"completed">]>;
|
|
85
|
+
}>>;
|
|
86
|
+
}>;
|
|
87
|
+
execute(_toolCallId: string, params: TodoParams, _signal?: AbortSignal, _onUpdate?: unknown, ctx?: ExtensionContext): Promise<{
|
|
88
|
+
content: {
|
|
89
|
+
type: "text";
|
|
90
|
+
text: string;
|
|
91
|
+
}[];
|
|
92
|
+
details: {
|
|
93
|
+
todos: TodoItem[];
|
|
94
|
+
};
|
|
95
|
+
isError: boolean;
|
|
96
|
+
} | {
|
|
97
|
+
content: {
|
|
98
|
+
type: "text";
|
|
99
|
+
text: string;
|
|
100
|
+
}[];
|
|
101
|
+
details: {
|
|
102
|
+
todos: TodoItem[];
|
|
103
|
+
};
|
|
104
|
+
isError?: undefined;
|
|
105
|
+
}>;
|
|
106
|
+
};
|
|
107
|
+
/** Wire the tool, the branch-replay events, and the /todos command. */
|
|
108
|
+
export declare function registerTodos(pi: ExtensionAPI): void;
|
|
109
|
+
export {};
|
|
110
|
+
//# sourceMappingURL=todos.d.ts.map
|