@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,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace MCP servers in YAGNI Code (YAG-446).
|
|
3
|
+
*
|
|
4
|
+
* The workspace's registered MCP servers (Connections → MCP Servers) are
|
|
5
|
+
* the config source: at session start we fetch the list from the backend
|
|
6
|
+
* and register one pi tool per enabled server tool, named with the same
|
|
7
|
+
* `mcp_<slug>__<tool>` wire shape the backend executor parses. Calls
|
|
8
|
+
* execute THROUGH the backend (`POST /api/yagni-code/mcp/call`), so
|
|
9
|
+
* credentials never reach this machine and the backend's egress guard,
|
|
10
|
+
* autonomy gate, per-user credential policy, and audit trail all apply.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is fail-soft against an older backend or an
|
|
13
|
+
* un-rescoped token: a 403/404/network failure on the list fetch means
|
|
14
|
+
* no MCP tools and no startup error.
|
|
15
|
+
*/
|
|
16
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
export interface McpToolInfo {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string | null;
|
|
20
|
+
inputSchema: unknown;
|
|
21
|
+
riskClass: string;
|
|
22
|
+
}
|
|
23
|
+
export interface McpServerInfo {
|
|
24
|
+
slug: string;
|
|
25
|
+
name: string;
|
|
26
|
+
tools: McpToolInfo[];
|
|
27
|
+
viewerCredential: {
|
|
28
|
+
source: "user" | "workspace";
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface McpClientOpts {
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
getToken: () => string | undefined;
|
|
34
|
+
fetchImpl?: typeof fetch;
|
|
35
|
+
}
|
|
36
|
+
export declare function mcpWireToolName(slug: string, tool: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch the workspace's MCP servers. Fail-soft by design: any non-OK
|
|
39
|
+
* response (older backend without the endpoints, token minted before the
|
|
40
|
+
* `yagni_code:mcp` scope existed) or transport error returns `[]`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function fetchMcpServers(opts: McpClientOpts): Promise<McpServerInfo[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Register every reachable MCP tool. Returns the wire names of MUTATING
|
|
45
|
+
* tools so the caller can extend the permission-gate policy — plan mode
|
|
46
|
+
* holds them, review mode confirms them, exactly like write/edit/bash.
|
|
47
|
+
*/
|
|
48
|
+
export declare function registerMcpTools(pi: ExtensionAPI, servers: McpServerInfo[], opts: McpClientOpts): {
|
|
49
|
+
mutatingToolNames: string[];
|
|
50
|
+
};
|
|
51
|
+
/** Render the /mcp listing (pure, for tests). */
|
|
52
|
+
export declare function renderMcpListing(servers: McpServerInfo[], baseUrl: string): string;
|
|
53
|
+
/** Wire the `/mcp` command: list servers, tools, and credential status. */
|
|
54
|
+
export declare function registerMcpCommand(pi: ExtensionAPI, servers: McpServerInfo[], opts: {
|
|
55
|
+
baseUrl: string;
|
|
56
|
+
}): void;
|
|
57
|
+
//# sourceMappingURL=mcpTools.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
3
|
+
export function mcpWireToolName(slug, tool) {
|
|
4
|
+
return `mcp_${slug}__${tool}`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Fetch the workspace's MCP servers. Fail-soft by design: any non-OK
|
|
8
|
+
* response (older backend without the endpoints, token minted before the
|
|
9
|
+
* `yagni_code:mcp` scope existed) or transport error returns `[]`.
|
|
10
|
+
*/
|
|
11
|
+
export async function fetchMcpServers(opts) {
|
|
12
|
+
try {
|
|
13
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/mcp/servers`, {
|
|
14
|
+
method: "GET",
|
|
15
|
+
headers: { authorization: `Bearer ${opts.getToken() ?? ""}` },
|
|
16
|
+
}, { fetchImpl: opts.fetchImpl });
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
return [];
|
|
19
|
+
const data = (await res.json());
|
|
20
|
+
if (!Array.isArray(data.servers))
|
|
21
|
+
return [];
|
|
22
|
+
return data.servers;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function schemaFor(tool) {
|
|
29
|
+
const schema = tool.inputSchema;
|
|
30
|
+
if (schema !== null &&
|
|
31
|
+
typeof schema === "object" &&
|
|
32
|
+
!Array.isArray(schema) &&
|
|
33
|
+
Object.keys(schema).length > 0) {
|
|
34
|
+
// MCP inputSchemas are plain JSON Schema objects, which is exactly what
|
|
35
|
+
// TypeBox schemas are at runtime — pass the server's schema through so
|
|
36
|
+
// the model sees real parameter shapes.
|
|
37
|
+
return schema;
|
|
38
|
+
}
|
|
39
|
+
return Type.Object({}, { additionalProperties: true });
|
|
40
|
+
}
|
|
41
|
+
function makeMcpTool(server, tool, opts) {
|
|
42
|
+
const wireName = mcpWireToolName(server.slug, tool.name);
|
|
43
|
+
return {
|
|
44
|
+
name: wireName,
|
|
45
|
+
label: `${server.name}: ${tool.name}`,
|
|
46
|
+
description: `${tool.description ?? `MCP tool ${tool.name}`} ` +
|
|
47
|
+
`(MCP server "${server.name}", risk: ${tool.riskClass}; runs through YAGNI)`,
|
|
48
|
+
parameters: schemaFor(tool),
|
|
49
|
+
async execute(_toolCallId, params, signal) {
|
|
50
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/mcp/call`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"content-type": "application/json",
|
|
54
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
slug: server.slug,
|
|
58
|
+
tool: tool.name,
|
|
59
|
+
args: params ?? {},
|
|
60
|
+
}),
|
|
61
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(await friendlyFetchError(wireName, res));
|
|
64
|
+
}
|
|
65
|
+
const result = (await res.json());
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
const suffix = result.errorCode === "user_credential_required"
|
|
68
|
+
? ` Bind your token at ${opts.baseUrl}/services/mcp.`
|
|
69
|
+
: "";
|
|
70
|
+
throw new Error(`${result.error ?? "MCP tool call failed"}${suffix}`);
|
|
71
|
+
}
|
|
72
|
+
const text = typeof result.result === "string"
|
|
73
|
+
? result.result
|
|
74
|
+
: JSON.stringify(result.result ?? null, null, 2);
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text }],
|
|
77
|
+
details: { riskClass: tool.riskClass },
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Register every reachable MCP tool. Returns the wire names of MUTATING
|
|
84
|
+
* tools so the caller can extend the permission-gate policy — plan mode
|
|
85
|
+
* holds them, review mode confirms them, exactly like write/edit/bash.
|
|
86
|
+
*/
|
|
87
|
+
export function registerMcpTools(pi, servers, opts) {
|
|
88
|
+
const mutatingToolNames = [];
|
|
89
|
+
for (const server of servers) {
|
|
90
|
+
for (const tool of server.tools) {
|
|
91
|
+
pi.registerTool(makeMcpTool(server, tool, opts));
|
|
92
|
+
if (tool.riskClass !== "read_only") {
|
|
93
|
+
mutatingToolNames.push(mcpWireToolName(server.slug, tool.name));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { mutatingToolNames };
|
|
98
|
+
}
|
|
99
|
+
/** Render the /mcp listing (pure, for tests). */
|
|
100
|
+
export function renderMcpListing(servers, baseUrl) {
|
|
101
|
+
if (servers.length === 0) {
|
|
102
|
+
return [
|
|
103
|
+
"No MCP servers connected for this workspace.",
|
|
104
|
+
`An admin can register one under Connections: ${baseUrl}/services/mcp`,
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
107
|
+
const lines = ["Workspace MCP servers:"];
|
|
108
|
+
for (const server of servers) {
|
|
109
|
+
const mutating = server.tools.filter((t) => t.riskClass !== "read_only").length;
|
|
110
|
+
const credential = server.viewerCredential.source === "user"
|
|
111
|
+
? "connected as you"
|
|
112
|
+
: "workspace credential";
|
|
113
|
+
lines.push(` ${server.name} (${server.slug}) — ${server.tools.length} tool${server.tools.length === 1 ? "" : "s"}` +
|
|
114
|
+
`${mutating > 0 ? ` (${mutating} mutating)` : ""} · ${credential}`);
|
|
115
|
+
for (const tool of server.tools) {
|
|
116
|
+
lines.push(` ${mcpWireToolName(server.slug, tool.name)} [${tool.riskClass}]`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
lines.push(`Bind a personal token (writes carry your authority): ${baseUrl}/services/mcp`);
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
/** Wire the `/mcp` command: list servers, tools, and credential status. */
|
|
123
|
+
export function registerMcpCommand(pi, servers, opts) {
|
|
124
|
+
pi.registerCommand("mcp", {
|
|
125
|
+
description: "List this workspace's MCP servers, their tools, and your credential status.",
|
|
126
|
+
handler: async (_args, ctx) => {
|
|
127
|
+
if (ctx.hasUI)
|
|
128
|
+
ctx.ui.notify(renderMcpListing(servers, opts.baseUrl), "info");
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=mcpTools.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import type { Citation } from "./askYagniTool.js";
|
|
4
|
+
/** Options for {@link makeSuggestNextWorkTool}. */
|
|
5
|
+
export interface MakeNextWorkToolOptions {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
getToken: () => string | undefined;
|
|
8
|
+
fetchImpl?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
/** A single offered next action: its id, the label shown, and the editor prompt it prefills. */
|
|
11
|
+
export interface NextActionOption {
|
|
12
|
+
id: "suggest_next_work" | "bootstrap" | "review";
|
|
13
|
+
/** Short label shown in the selector. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** The message prefilled into the editor when this action is chosen. */
|
|
16
|
+
prompt: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The one default next action plus its escape hatches. `primary` is the single
|
|
20
|
+
* most-likely thing to do (ask @yagni what to work on next); `escapeHatches` are
|
|
21
|
+
* the two alternatives (bootstrap a first ticket, or review the working diff).
|
|
22
|
+
* This is deliberately a ONE-default-plus-hatches shape, NOT a flat four-option
|
|
23
|
+
* chooser — defaults over choices (spec §7.3).
|
|
24
|
+
*/
|
|
25
|
+
export interface DefaultNextAction {
|
|
26
|
+
primary: NextActionOption;
|
|
27
|
+
escapeHatches: NextActionOption[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the default next action offered after the init pass. The recommended
|
|
31
|
+
* default is `suggest_next_work` ("ask @yagni what to work on next"); the escape
|
|
32
|
+
* hatches are "bootstrap a first ticket and /go" and "review this diff". Pure so
|
|
33
|
+
* the presenter can render it without a network.
|
|
34
|
+
*/
|
|
35
|
+
export declare function defaultNextAction(): DefaultNextAction;
|
|
36
|
+
declare const parameters: Type.TObject<{
|
|
37
|
+
context: Type.TOptional<Type.TString>;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Build the `suggest_next_work` tool.
|
|
41
|
+
*
|
|
42
|
+
* Asks the YAGNI app for the highest-value next thing to work on, RANKED BY
|
|
43
|
+
* BUSINESS PRIORITY across open work-items and desk next-moves (not by ticket
|
|
44
|
+
* order), with the business case for the top pick. Returns a ranked
|
|
45
|
+
* recommendation plus citations.
|
|
46
|
+
*/
|
|
47
|
+
export declare function makeSuggestNextWorkTool(opts: MakeNextWorkToolOptions): ToolDefinition<typeof parameters, {
|
|
48
|
+
citations: Citation[];
|
|
49
|
+
}>;
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=nextWorkTool.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build the default next action offered after the init pass. The recommended
|
|
5
|
+
* default is `suggest_next_work` ("ask @yagni what to work on next"); the escape
|
|
6
|
+
* hatches are "bootstrap a first ticket and /go" and "review this diff". Pure so
|
|
7
|
+
* the presenter can render it without a network.
|
|
8
|
+
*/
|
|
9
|
+
export function defaultNextAction() {
|
|
10
|
+
return {
|
|
11
|
+
primary: {
|
|
12
|
+
id: "suggest_next_work",
|
|
13
|
+
label: "Ask @yagni what to work on next (recommended)",
|
|
14
|
+
prompt: "What should I work on next?",
|
|
15
|
+
},
|
|
16
|
+
escapeHatches: [
|
|
17
|
+
{
|
|
18
|
+
id: "bootstrap",
|
|
19
|
+
label: "Bootstrap: pick a first ticket and /go",
|
|
20
|
+
prompt: "Pick a good first ticket to bootstrap on, then run /go on it.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "review",
|
|
24
|
+
label: "Review this diff",
|
|
25
|
+
prompt: "Review the current working-tree diff for business fit and correctness.",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const parameters = Type.Object({
|
|
31
|
+
context: Type.Optional(Type.String()),
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Build the `suggest_next_work` tool.
|
|
35
|
+
*
|
|
36
|
+
* Asks the YAGNI app for the highest-value next thing to work on, RANKED BY
|
|
37
|
+
* BUSINESS PRIORITY across open work-items and desk next-moves (not by ticket
|
|
38
|
+
* order), with the business case for the top pick. Returns a ranked
|
|
39
|
+
* recommendation plus citations.
|
|
40
|
+
*/
|
|
41
|
+
export function makeSuggestNextWorkTool(opts) {
|
|
42
|
+
return {
|
|
43
|
+
name: "suggest_next_work",
|
|
44
|
+
label: "Suggest next work",
|
|
45
|
+
description: "Ask YAGNI what to work on next, ranked by BUSINESS priority across open work-items " +
|
|
46
|
+
"and desk next-moves (not just ticket order), with the business case for the top " +
|
|
47
|
+
"pick and a concrete first step. Optionally pass a `context` to focus the ranking " +
|
|
48
|
+
"(e.g. 'revenue this week'). Use when deciding what to pick up.",
|
|
49
|
+
promptSnippet: "suggest_next_work: ask YAGNI for the highest-value next task, ranked by business priority.",
|
|
50
|
+
promptGuidelines: [
|
|
51
|
+
"Call suggest_next_work when the user asks what to do next or when you've finished and need the next task.",
|
|
52
|
+
"Pass a `context` if the user has a focus (a goal, a deadline, an area) so the ranking reflects it.",
|
|
53
|
+
"Present the recommendation with its business case; let the user confirm before starting.",
|
|
54
|
+
],
|
|
55
|
+
parameters,
|
|
56
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
57
|
+
onUpdate?.({
|
|
58
|
+
content: [{ type: "text", text: "Ranking the highest-value next work…" }],
|
|
59
|
+
details: { citations: [] },
|
|
60
|
+
});
|
|
61
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/next-action`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
"content-type": "application/json",
|
|
65
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({ context: params.context, cwd: ctx?.cwd }),
|
|
68
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new Error(await friendlyFetchError("suggest_next_work", res));
|
|
71
|
+
}
|
|
72
|
+
const data = (await res.json());
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: data.suggestion }],
|
|
75
|
+
details: { citations: data.citations ?? [] },
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=nextWorkTool.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P3 - interactive permission tiers + plan mode, wired to pi's tool-call gate.
|
|
3
|
+
*
|
|
4
|
+
* YAGNI Code registers no tool_call handler today, so the interactive session has
|
|
5
|
+
* no plan/approval surface. P3 adds one on pi's documented `tool_call` block seam
|
|
6
|
+
* plus a `/mode` command:
|
|
7
|
+
* - auto (default): never blocks. Byte-identical to today, so this is additive.
|
|
8
|
+
* - plan : blocks write/edit/bash so the agent can explore + propose without
|
|
9
|
+
* touching the tree.
|
|
10
|
+
* - review : surfaces a three-way ctx.ui.select before a write/edit/bash; a
|
|
11
|
+
* session bless (policy.isBlessed) auto-approves an action the user
|
|
12
|
+
* already OK'd for this tool + directory, so only novel actions
|
|
13
|
+
* prompt. Picking "Yes, and don't ask again for <tool> in <dir>"
|
|
14
|
+
* adds a session-scoped bless rule AND drafts a decision capture.
|
|
15
|
+
*
|
|
16
|
+
* `decideGate` is PURE; the live wiring holds the mode in a small closure (no
|
|
17
|
+
* module-global state). The default auto mode remains fail-open, but stricter
|
|
18
|
+
* modes fail closed for side-effect tools if the gate itself errors. Bless rules
|
|
19
|
+
* are session-scoped, path-prefix-bound, never persisted, and never consulted in
|
|
20
|
+
* plan mode (plan blocks outright before isBlessed is reached).
|
|
21
|
+
*
|
|
22
|
+
* Plan mode is a full mode, not just a blocker: while active, every agent turn
|
|
23
|
+
* carries a hidden [PLAN MODE ACTIVE] context message so the model plans
|
|
24
|
+
* instead of flailing against held writes, and a footer chip shows the mode.
|
|
25
|
+
* When the mode leaves plan, stale plan-context messages are filtered out of
|
|
26
|
+
* the context so the model doesn't keep believing it is restricted.
|
|
27
|
+
*/
|
|
28
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { type BlessStore } from "./bless.js";
|
|
30
|
+
export type PermissionMode = "auto" | "plan" | "review";
|
|
31
|
+
/** Which tools each tier acts on, plus the optional grounding-bless predicate. */
|
|
32
|
+
export interface PermissionPolicy {
|
|
33
|
+
/** Tools blocked outright in plan mode (write/exec). */
|
|
34
|
+
planBlockTools: string[];
|
|
35
|
+
/** Tools that prompt for confirmation in review mode. */
|
|
36
|
+
reviewConfirmTools: string[];
|
|
37
|
+
/**
|
|
38
|
+
* Optional: a recorded decision already blesses this action, so it auto-runs in
|
|
39
|
+
* review mode instead of prompting. The hook for tying the gate to captured
|
|
40
|
+
* judgment; default undefined (nothing pre-blessed).
|
|
41
|
+
*/
|
|
42
|
+
isBlessed?: (toolName: string, params: Record<string, unknown>) => boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare const DEFAULT_PERMISSION_POLICY: PermissionPolicy;
|
|
45
|
+
/** A pure gate verdict: block outright, ask to confirm, or allow. */
|
|
46
|
+
export interface GateDecision {
|
|
47
|
+
block: boolean;
|
|
48
|
+
reason?: string;
|
|
49
|
+
/** review mode only: the caller should ctx.ui.confirm before allowing. */
|
|
50
|
+
confirm?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Pure permission decision for one tool call under a mode + policy. auto always
|
|
54
|
+
* allows; plan blocks the write/exec set; review marks writes for confirmation
|
|
55
|
+
* unless a recorded decision blesses them.
|
|
56
|
+
*/
|
|
57
|
+
export declare function decideGate(toolName: string, params: Record<string, unknown>, mode: PermissionMode, policy: PermissionPolicy): GateDecision;
|
|
58
|
+
/** What was blessed with "don't ask again", handed to the capture hook. */
|
|
59
|
+
export interface BlessRememberInfo {
|
|
60
|
+
tool: string;
|
|
61
|
+
/** A human-readable directory label (cwd-relative when inside the tree). */
|
|
62
|
+
dir: string;
|
|
63
|
+
}
|
|
64
|
+
export interface RegisterPermissionDeps {
|
|
65
|
+
policy?: PermissionPolicy;
|
|
66
|
+
/** Initial mode (default auto). */
|
|
67
|
+
mode?: PermissionMode;
|
|
68
|
+
/** Session bless store factory (needs the cwd from the first tool_call). */
|
|
69
|
+
makeBlessStore?: (cwd: string) => BlessStore;
|
|
70
|
+
/**
|
|
71
|
+
* Called (fire-and-forget) when the user picks "Yes, and don't ask again".
|
|
72
|
+
* The judgment-capture hook: draft a decision so YAGNI remembers. Fail-soft;
|
|
73
|
+
* never blocks the approved tool call.
|
|
74
|
+
*/
|
|
75
|
+
onBlessRemember?: (ctx: ExtensionContext, info: BlessRememberInfo) => void | Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/** The customType tag on injected plan-mode context (filterable later). */
|
|
78
|
+
export declare const PLAN_CONTEXT_TYPE = "yagni-plan-context";
|
|
79
|
+
export declare const PLAN_CONTEXT_MESSAGE = "[PLAN MODE ACTIVE]\nYou are in plan mode: explore and design, change nothing.\n- write, edit, and bash are held by the permission gate; do not attempt them.\n- Read, search, and ask_yagni freely to ground the plan in how this company works.\n- Produce a concrete numbered plan of the steps you would take, with the files involved.\n- End by asking the user to review the plan; they run /mode auto (or /mode review) to execute it.\n- Once executing, track the plan's steps with todo_write.";
|
|
80
|
+
/**
|
|
81
|
+
* Drop previously injected plan-mode context once plan mode is off, so the
|
|
82
|
+
* model stops believing writes are held. Pure; returns the SAME array when
|
|
83
|
+
* nothing needs filtering so callers can cheaply detect a no-op.
|
|
84
|
+
*/
|
|
85
|
+
export declare function filterStalePlanContext<T>(messages: T[]): T[];
|
|
86
|
+
/**
|
|
87
|
+
* Wire the tool_call gate + the /mode command onto a shared mode holder. Default
|
|
88
|
+
* auto, so absent any /mode this is a no-op over today's behavior.
|
|
89
|
+
*/
|
|
90
|
+
export declare function registerPermissionGate(pi: ExtensionAPI, deps?: RegisterPermissionDeps): void;
|
|
91
|
+
//# sourceMappingURL=permission.d.ts.map
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P3 - interactive permission tiers + plan mode, wired to pi's tool-call gate.
|
|
3
|
+
*
|
|
4
|
+
* YAGNI Code registers no tool_call handler today, so the interactive session has
|
|
5
|
+
* no plan/approval surface. P3 adds one on pi's documented `tool_call` block seam
|
|
6
|
+
* plus a `/mode` command:
|
|
7
|
+
* - auto (default): never blocks. Byte-identical to today, so this is additive.
|
|
8
|
+
* - plan : blocks write/edit/bash so the agent can explore + propose without
|
|
9
|
+
* touching the tree.
|
|
10
|
+
* - review : surfaces a three-way ctx.ui.select before a write/edit/bash; a
|
|
11
|
+
* session bless (policy.isBlessed) auto-approves an action the user
|
|
12
|
+
* already OK'd for this tool + directory, so only novel actions
|
|
13
|
+
* prompt. Picking "Yes, and don't ask again for <tool> in <dir>"
|
|
14
|
+
* adds a session-scoped bless rule AND drafts a decision capture.
|
|
15
|
+
*
|
|
16
|
+
* `decideGate` is PURE; the live wiring holds the mode in a small closure (no
|
|
17
|
+
* module-global state). The default auto mode remains fail-open, but stricter
|
|
18
|
+
* modes fail closed for side-effect tools if the gate itself errors. Bless rules
|
|
19
|
+
* are session-scoped, path-prefix-bound, never persisted, and never consulted in
|
|
20
|
+
* plan mode (plan blocks outright before isBlessed is reached).
|
|
21
|
+
*
|
|
22
|
+
* Plan mode is a full mode, not just a blocker: while active, every agent turn
|
|
23
|
+
* carries a hidden [PLAN MODE ACTIVE] context message so the model plans
|
|
24
|
+
* instead of flailing against held writes, and a footer chip shows the mode.
|
|
25
|
+
* When the mode leaves plan, stale plan-context messages are filtered out of
|
|
26
|
+
* the context so the model doesn't keep believing it is restricted.
|
|
27
|
+
*/
|
|
28
|
+
import { makeBlessStore as defaultMakeBlessStore } from "./bless.js";
|
|
29
|
+
export const DEFAULT_PERMISSION_POLICY = {
|
|
30
|
+
planBlockTools: ["write", "edit", "bash"],
|
|
31
|
+
reviewConfirmTools: ["write", "edit", "bash"],
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Pure permission decision for one tool call under a mode + policy. auto always
|
|
35
|
+
* allows; plan blocks the write/exec set; review marks writes for confirmation
|
|
36
|
+
* unless a recorded decision blesses them.
|
|
37
|
+
*/
|
|
38
|
+
export function decideGate(toolName, params, mode, policy) {
|
|
39
|
+
if (mode === "auto")
|
|
40
|
+
return { block: false };
|
|
41
|
+
if (mode === "plan") {
|
|
42
|
+
if (policy.planBlockTools.includes(toolName)) {
|
|
43
|
+
return {
|
|
44
|
+
block: true,
|
|
45
|
+
reason: `plan mode: ${toolName} is a write or exec action and is held. Switch to /mode auto to apply changes.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { block: false };
|
|
49
|
+
}
|
|
50
|
+
// review
|
|
51
|
+
if (policy.reviewConfirmTools.includes(toolName)) {
|
|
52
|
+
if (policy.isBlessed?.(toolName, params))
|
|
53
|
+
return { block: false };
|
|
54
|
+
return { block: false, confirm: true };
|
|
55
|
+
}
|
|
56
|
+
return { block: false };
|
|
57
|
+
}
|
|
58
|
+
/** The customType tag on injected plan-mode context (filterable later). */
|
|
59
|
+
export const PLAN_CONTEXT_TYPE = "yagni-plan-context";
|
|
60
|
+
const PLAN_MARKER = "[PLAN MODE ACTIVE]";
|
|
61
|
+
export const PLAN_CONTEXT_MESSAGE = `${PLAN_MARKER}
|
|
62
|
+
You are in plan mode: explore and design, change nothing.
|
|
63
|
+
- write, edit, and bash are held by the permission gate; do not attempt them.
|
|
64
|
+
- Read, search, and ask_yagni freely to ground the plan in how this company works.
|
|
65
|
+
- Produce a concrete numbered plan of the steps you would take, with the files involved.
|
|
66
|
+
- End by asking the user to review the plan; they run /mode auto (or /mode review) to execute it.
|
|
67
|
+
- Once executing, track the plan's steps with todo_write.`;
|
|
68
|
+
function mentionsPlanMarker(content) {
|
|
69
|
+
if (typeof content === "string")
|
|
70
|
+
return content.includes(PLAN_MARKER);
|
|
71
|
+
if (Array.isArray(content)) {
|
|
72
|
+
return content.some((c) => typeof c?.text === "string" &&
|
|
73
|
+
(c.text.includes(PLAN_MARKER)));
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Drop previously injected plan-mode context once plan mode is off, so the
|
|
79
|
+
* model stops believing writes are held. Pure; returns the SAME array when
|
|
80
|
+
* nothing needs filtering so callers can cheaply detect a no-op.
|
|
81
|
+
*/
|
|
82
|
+
export function filterStalePlanContext(messages) {
|
|
83
|
+
const keep = messages.filter((m) => {
|
|
84
|
+
const msg = m;
|
|
85
|
+
if (msg?.customType === PLAN_CONTEXT_TYPE)
|
|
86
|
+
return false;
|
|
87
|
+
if (msg?.role === "user" && mentionsPlanMarker(msg.content))
|
|
88
|
+
return false;
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
return keep.length === messages.length ? messages : keep;
|
|
92
|
+
}
|
|
93
|
+
const MODE_STATUS = {
|
|
94
|
+
auto: undefined,
|
|
95
|
+
plan: "⏸ plan",
|
|
96
|
+
review: "✓ review",
|
|
97
|
+
};
|
|
98
|
+
const MODE_COPY = {
|
|
99
|
+
auto: "auto: changes apply without prompting (default).",
|
|
100
|
+
plan: "plan: write, edit, and bash are held so the agent can explore and propose only.",
|
|
101
|
+
review: "review: you confirm each write, edit, or bash command before it applies.",
|
|
102
|
+
};
|
|
103
|
+
function isMode(value) {
|
|
104
|
+
return value === "auto" || value === "plan" || value === "review";
|
|
105
|
+
}
|
|
106
|
+
function sideEffectTools(policy) {
|
|
107
|
+
return new Set([...policy.planBlockTools, ...policy.reviewConfirmTools]);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Wire the tool_call gate + the /mode command onto a shared mode holder. Default
|
|
111
|
+
* auto, so absent any /mode this is a no-op over today's behavior.
|
|
112
|
+
*/
|
|
113
|
+
export function registerPermissionGate(pi, deps = {}) {
|
|
114
|
+
const basePolicy = deps.policy ?? DEFAULT_PERMISSION_POLICY;
|
|
115
|
+
let mode = deps.mode ?? "auto";
|
|
116
|
+
const makeStore = deps.makeBlessStore ?? defaultMakeBlessStore;
|
|
117
|
+
// The session bless store is created lazily on the first tool_call (it needs
|
|
118
|
+
// the cwd). Its isBlessed backs the review-mode auto-approve, UNLESS the caller
|
|
119
|
+
// injected its own isBlessed (e.g. a test policy) — that always wins.
|
|
120
|
+
let blessStore = null;
|
|
121
|
+
const effectivePolicy = {
|
|
122
|
+
planBlockTools: basePolicy.planBlockTools,
|
|
123
|
+
reviewConfirmTools: basePolicy.reviewConfirmTools,
|
|
124
|
+
isBlessed: basePolicy.isBlessed ?? ((tool, params) => blessStore?.isBlessed(tool, params) ?? false),
|
|
125
|
+
};
|
|
126
|
+
const sideEffects = sideEffectTools(effectivePolicy);
|
|
127
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
128
|
+
try {
|
|
129
|
+
const input = event.input ?? {};
|
|
130
|
+
const decision = decideGate(event.toolName, input, mode, effectivePolicy);
|
|
131
|
+
if (decision.block)
|
|
132
|
+
return { block: true, reason: decision.reason };
|
|
133
|
+
if (decision.confirm) {
|
|
134
|
+
// Review mode needs a confirmation. With no dialog-capable UI (headless),
|
|
135
|
+
// fail CLOSED: the user explicitly chose a stricter mode, so a write we
|
|
136
|
+
// cannot get consent for is held rather than silently auto-applied (this
|
|
137
|
+
// mirrors plan mode, which blocks regardless of UI).
|
|
138
|
+
if (!ctx?.hasUI) {
|
|
139
|
+
return { block: true, reason: `review mode: ${event.toolName} held (no UI to confirm). Switch to /mode auto to apply.` };
|
|
140
|
+
}
|
|
141
|
+
// Lazily bind the bless store to this session's cwd.
|
|
142
|
+
if (!blessStore)
|
|
143
|
+
blessStore = makeStore(ctx.cwd ?? ".");
|
|
144
|
+
// Three-way prompt (pi's confirm is boolean-only, so use select): Yes,
|
|
145
|
+
// Yes-and-remember (only when a path-prefix bless is meaningful), or No.
|
|
146
|
+
const dir = blessStore.describeDir(input);
|
|
147
|
+
const blessable = dir !== null;
|
|
148
|
+
const yes = "Yes";
|
|
149
|
+
const no = "No";
|
|
150
|
+
const remember = blessable
|
|
151
|
+
? `Yes, and don't ask again for ${event.toolName} in ${dir}`
|
|
152
|
+
: undefined;
|
|
153
|
+
const options = blessable ? [yes, remember, no] : [yes, no];
|
|
154
|
+
const choice = await ctx.ui.select("YAGNI Code review mode", options);
|
|
155
|
+
if (choice === yes)
|
|
156
|
+
return {};
|
|
157
|
+
if (blessable && choice === remember) {
|
|
158
|
+
// Session-scoped bless, then a fire-and-forget decision capture so the
|
|
159
|
+
// approved tool call is never delayed by the capture prompt.
|
|
160
|
+
blessStore.add(event.toolName, input);
|
|
161
|
+
if (deps.onBlessRemember) {
|
|
162
|
+
void Promise.resolve(deps.onBlessRemember(ctx, { tool: event.toolName, dir: dir })).catch(() => { });
|
|
163
|
+
}
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
// "No" or a dismissed dialog (undefined) → held.
|
|
167
|
+
return { block: true, reason: `declined ${event.toolName} in review mode` };
|
|
168
|
+
}
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
if (mode !== "auto" && sideEffects.has(event.toolName)) {
|
|
173
|
+
return {
|
|
174
|
+
block: true,
|
|
175
|
+
reason: `permission gate failed while ${mode} mode was active; held ${event.toolName}. Switch to /mode auto to apply.`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
// Model awareness: while plan mode is on, every agent start carries a hidden
|
|
182
|
+
// plan-mode context message so the model plans instead of attempting held
|
|
183
|
+
// writes. Off-plan turns inject nothing.
|
|
184
|
+
pi.on("before_agent_start", async () => {
|
|
185
|
+
if (mode !== "plan")
|
|
186
|
+
return;
|
|
187
|
+
return {
|
|
188
|
+
message: { customType: PLAN_CONTEXT_TYPE, content: PLAN_CONTEXT_MESSAGE, display: false },
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
// Once plan mode is off, strip the stale plan context so the model does not
|
|
192
|
+
// keep believing writes are held.
|
|
193
|
+
pi.on("context", async (event) => {
|
|
194
|
+
if (mode === "plan")
|
|
195
|
+
return;
|
|
196
|
+
const filtered = filterStalePlanContext(event.messages);
|
|
197
|
+
if (filtered === event.messages)
|
|
198
|
+
return;
|
|
199
|
+
return { messages: filtered };
|
|
200
|
+
});
|
|
201
|
+
const paintMode = (ctx) => {
|
|
202
|
+
try {
|
|
203
|
+
if (ctx.hasUI)
|
|
204
|
+
ctx.ui.setStatus?.("yagni-mode", MODE_STATUS[mode]);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// The chip is chrome; never let it break /mode.
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
pi.registerCommand("mode", {
|
|
211
|
+
description: "Set the permission tier: /mode auto | plan | review. No argument shows the current mode.",
|
|
212
|
+
handler: async (args, ctx) => {
|
|
213
|
+
const notify = (m, t) => {
|
|
214
|
+
if (ctx.hasUI)
|
|
215
|
+
ctx.ui.notify(m, t);
|
|
216
|
+
};
|
|
217
|
+
const arg = args.trim().toLowerCase();
|
|
218
|
+
if (!arg) {
|
|
219
|
+
notify(`Permission mode is ${mode}. ${MODE_COPY[mode]}`, "info");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!isMode(arg)) {
|
|
223
|
+
notify(`Unknown mode "${arg}". Use auto, plan, or review.`, "warning");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const leavingPlan = mode === "plan" && arg !== "plan";
|
|
227
|
+
mode = arg;
|
|
228
|
+
paintMode(ctx);
|
|
229
|
+
notify(`Permission mode set to ${mode}. ${MODE_COPY[mode]}`, "info");
|
|
230
|
+
if (leavingPlan) {
|
|
231
|
+
notify("Plan mode off. Tell the agent to execute the plan; it tracks steps with /todos.", "info");
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=permission.js.map
|