@yagni-app/code-staging 0.0.0 → 0.1.0-staging.997.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/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- 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 +188 -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 +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -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 +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -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 +168 -0
- package/dist/extension/pipeline/eval.js +182 -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 +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -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 +636 -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/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 +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -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 +51 -0
- package/dist/extension/pipeline/stages.js +113 -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 +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -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 +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -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/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/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 +135 -0
- package/dist/extension/subagents.js +281 -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 +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +39 -3
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ask_advisor` — the peak-tier escalation available to Balanced sessions (YAG-380).
|
|
3
|
+
*
|
|
4
|
+
* Cheap driver, expensive consultant: a Balanced session drives on `advanced` and
|
|
5
|
+
* calls this when it hits a judgment call worth the strongest model. The consult
|
|
6
|
+
* spawns a child pi at `peak` through the same `runStage` seam `/go` uses, and
|
|
7
|
+
* returns the advice as plain TEXT the driver acts on — text we own, so
|
|
8
|
+
* `record_decision` can bank it into the corpus afterwards.
|
|
9
|
+
*
|
|
10
|
+
* Two deliberate design calls, both of which cost money and buy quality:
|
|
11
|
+
*
|
|
12
|
+
* 1. The advisor gets REAL read-only recon (read/grep/find/ls), the same set
|
|
13
|
+
* the plan stage uses. An advisor limited to what the driver pre-digested
|
|
14
|
+
* rubber-stamps the driver's framing; half the value of escalation is
|
|
15
|
+
* looking where the driver did not think to look. This makes a consult an
|
|
16
|
+
* agentic loop, so a thorough one on a large repo runs $0.40-$1.00 rather
|
|
17
|
+
* than the ~4c a single-shot completion would cost. That is the price of
|
|
18
|
+
* the advisor being useful. It gets NO bash: a consultant reasons and
|
|
19
|
+
* reads, it must not build, run, or write.
|
|
20
|
+
* 2. The input schema is NARROW on purpose — question, what was tried,
|
|
21
|
+
* relevant excerpts. There is deliberately no field a conversation
|
|
22
|
+
* transcript fits into, because passing the transcript is how a consult
|
|
23
|
+
* silently becomes more expensive than just running peak.
|
|
24
|
+
*
|
|
25
|
+
* The gate, the cap and the spend ceiling live in `advisor.ts` and are pure;
|
|
26
|
+
* this module is the I/O.
|
|
27
|
+
*/
|
|
28
|
+
import { Type } from "typebox";
|
|
29
|
+
import { ADVISOR_MODEL_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatConsultCost, } from "./advisor.js";
|
|
30
|
+
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
31
|
+
/**
|
|
32
|
+
* Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
|
|
33
|
+
* reason documented there: enough tools to navigate instead of flailing on a
|
|
34
|
+
* single `read`. No `bash`.
|
|
35
|
+
*/
|
|
36
|
+
export const ADVISOR_TOOLS = ["read", "grep", "find", "ls", "ask_yagni"];
|
|
37
|
+
const parameters = Type.Object({
|
|
38
|
+
question: Type.String({
|
|
39
|
+
description: "The single judgment call you want a second opinion on. Be specific.",
|
|
40
|
+
}),
|
|
41
|
+
tried: Type.Optional(Type.String({ description: "What you have already tried or ruled out, and why." })),
|
|
42
|
+
excerpts: Type.Optional(Type.String({ description: "Relevant code excerpts or file paths. Not a transcript." })),
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Assemble the consult brief. The advisor's persona already tells it not to take
|
|
46
|
+
* this framing on trust, so the brief orients rather than constrains.
|
|
47
|
+
*/
|
|
48
|
+
export function buildConsultBrief(params) {
|
|
49
|
+
const parts = [`Question: ${params.question}`];
|
|
50
|
+
if (params.tried?.trim())
|
|
51
|
+
parts.push(`Already tried / ruled out:\n${params.tried.trim()}`);
|
|
52
|
+
if (params.excerpts?.trim())
|
|
53
|
+
parts.push(`Relevant excerpts:\n${params.excerpts.trim()}`);
|
|
54
|
+
parts.push("Read the code yourself before answering. Do not assume the framing above is complete.");
|
|
55
|
+
return parts.join("\n\n");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The synthetic stage a consult runs as.
|
|
59
|
+
*
|
|
60
|
+
* `id` reuses the existing `plan` StageId rather than adding an advisor member:
|
|
61
|
+
* the advisor is NOT a pipeline stage, and a new StageId would ripple into the
|
|
62
|
+
* feed/activity reducers that switch over it. `agent` is what selects the
|
|
63
|
+
* persona, and that is the advisor's own.
|
|
64
|
+
*/
|
|
65
|
+
export function advisorStage() {
|
|
66
|
+
return {
|
|
67
|
+
id: "plan",
|
|
68
|
+
agent: "advisor",
|
|
69
|
+
model: ADVISOR_MODEL_TIER,
|
|
70
|
+
tools: ADVISOR_TOOLS,
|
|
71
|
+
taskTemplate: "{ticket}",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function makeAskAdvisorTool(opts) {
|
|
75
|
+
const limits = opts.limits ?? DEFAULT_ADVISOR_LIMITS;
|
|
76
|
+
const runStage = opts.runStage ?? defaultRunStage;
|
|
77
|
+
return {
|
|
78
|
+
name: "ask_advisor",
|
|
79
|
+
label: "Ask the advisor",
|
|
80
|
+
description: "Escalate ONE hard judgment call to the peak-tier advisor — the strongest " +
|
|
81
|
+
"model available, which reads the code itself and returns a recommendation. " +
|
|
82
|
+
"Available only on the Balanced tier, and capped per session, so use it for " +
|
|
83
|
+
"calls that are genuinely worth it: an architectural fork with no obvious " +
|
|
84
|
+
"right answer, a subtle correctness question you cannot settle by reading, " +
|
|
85
|
+
"a change whose blast radius you are unsure of, or a second opinion before " +
|
|
86
|
+
"committing to an approach you would have to unwind. Do NOT use it for " +
|
|
87
|
+
"lookups (use ask_yagni), for anything you can settle by reading the code, " +
|
|
88
|
+
"or to review work you have already finished. Pass a sharp question plus the " +
|
|
89
|
+
"relevant excerpts — never a conversation transcript.",
|
|
90
|
+
promptSnippet: "ask_advisor: escalate one hard judgment call to the peak-tier advisor (Balanced sessions, capped).",
|
|
91
|
+
promptGuidelines: [
|
|
92
|
+
"Call ask_advisor only for a genuine judgment fork — an architectural choice, a subtle correctness question, or a second opinion before an approach you would have to unwind. Reading the code is cheaper; do that first.",
|
|
93
|
+
"Ask ONE specific question per consult, and include the excerpts that matter. The advisor reads the repo itself, so point it at the right place rather than pasting everything.",
|
|
94
|
+
"Consults are capped per session. Spend them on the calls you would otherwise get wrong.",
|
|
95
|
+
"The advice comes back as plain text: act on it, and call record_decision when it settles a product-intent call so the next agent inherits it.",
|
|
96
|
+
],
|
|
97
|
+
parameters,
|
|
98
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
99
|
+
// Read the LIVE session model: pi's picker can change it after this tool
|
|
100
|
+
// was registered, in both directions.
|
|
101
|
+
const decision = decideConsult({
|
|
102
|
+
model: ctx?.model?.id,
|
|
103
|
+
state: opts.state.read(),
|
|
104
|
+
limits,
|
|
105
|
+
});
|
|
106
|
+
if (!decision.allow) {
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: decision.reason ?? "ask_advisor is unavailable." }],
|
|
109
|
+
details: { consults: opts.state.read().consults, cost: 0 },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
onUpdate?.({
|
|
113
|
+
content: [{ type: "text", text: "Consulting the advisor…" }],
|
|
114
|
+
details: { consults: opts.state.read().consults, cost: 0 },
|
|
115
|
+
});
|
|
116
|
+
const result = await runStage(advisorStage(), { ticket: buildConsultBrief(params) }, { cwd: ctx?.cwd ?? process.cwd(), ...(signal ? { signal } : {}) });
|
|
117
|
+
const cost = result.usage?.cost ?? 0;
|
|
118
|
+
const state = opts.state.record(cost);
|
|
119
|
+
if (result.exitCode !== 0 && !result.finalOutput.trim()) {
|
|
120
|
+
// Fail honestly rather than returning an empty recommendation. The
|
|
121
|
+
// consult still counts: it spawned, and it may well have spent.
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: `The advisor consult failed (exit ${result.exitCode}). ` +
|
|
127
|
+
`${formatConsultCost({ cost, consults: state.consults, limits })} ` +
|
|
128
|
+
`Decide this one yourself.`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
details: { consults: state.consults, cost },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: `${result.finalOutput.trim()}\n\n${formatConsultCost({ cost, consults: state.consults, limits })}`,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
details: { consults: state.consults, cost },
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* `/advise <question>` — the manual escalation lever.
|
|
148
|
+
*
|
|
149
|
+
* Runs the SAME tool the model calls, sharing one state handle, so a manual
|
|
150
|
+
* consult draws on the same cap rather than opening a side channel around it.
|
|
151
|
+
* The advice is sent into the conversation (like /go-compare's report) so the
|
|
152
|
+
* driver sees it in context and can act on it.
|
|
153
|
+
*/
|
|
154
|
+
export function registerAdviseCommand(pi, tool) {
|
|
155
|
+
pi.registerCommand("advise", {
|
|
156
|
+
description: "Escalate one hard call to the peak-tier advisor (Balanced sessions only, capped per session).",
|
|
157
|
+
handler: async (args, ctx) => {
|
|
158
|
+
const notify = (message, type) => {
|
|
159
|
+
if (ctx.hasUI)
|
|
160
|
+
ctx.ui.notify(message, type);
|
|
161
|
+
};
|
|
162
|
+
const question = args.trim();
|
|
163
|
+
if (!question) {
|
|
164
|
+
notify("Usage: /advise <the judgment call you want a second opinion on>", "warning");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!ctx.isIdle()) {
|
|
168
|
+
notify("YAGNI Code is busy; wait for the current turn to finish before /advise.", "warning");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
notify("Consulting the peak-tier advisor. This reads the repo, so it takes a moment.", "info");
|
|
172
|
+
try {
|
|
173
|
+
const out = await tool.execute("advise", { question }, ctx.signal, undefined, ctx);
|
|
174
|
+
const text = out.content
|
|
175
|
+
.map((c) => c.text ?? "")
|
|
176
|
+
.join("\n")
|
|
177
|
+
.trim();
|
|
178
|
+
await pi.sendUserMessage(text || "The advisor returned nothing.");
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
182
|
+
notify(`/advise failed: ${message}`, "error");
|
|
183
|
+
await pi.sendUserMessage(`/advise failed: ${message}`);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=askAdvisorTool.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { type RepoDocSnippet } from "./repoDocs.js";
|
|
4
|
+
/** A single source citation returned by the YAGNI `ask` endpoint. */
|
|
5
|
+
export interface Citation {
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
/** Options for {@link makeAskYagniTool}. */
|
|
10
|
+
export interface MakeAskYagniToolOptions {
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
getToken: () => string | undefined;
|
|
13
|
+
fetchImpl?: typeof fetch;
|
|
14
|
+
/**
|
|
15
|
+
* Repo-doc grounding seam (default: the real bounded collector). The tool
|
|
16
|
+
* sends the working tree's most relevant docs (ADRs, CONTEXT.md, AGENTS.md,
|
|
17
|
+
* docs/*.md) alongside the question so the backend can ground its answer in
|
|
18
|
+
* repo-level knowledge the workspace corpus does not hold — always at the
|
|
19
|
+
* checkout's own era, since the docs are read from the tree being edited.
|
|
20
|
+
*/
|
|
21
|
+
collectDocs?: (cwd: string, query: string) => RepoDocSnippet[];
|
|
22
|
+
}
|
|
23
|
+
declare const parameters: Type.TObject<{
|
|
24
|
+
question: Type.TString;
|
|
25
|
+
context: Type.TOptional<Type.TString>;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Build the `ask_yagni` tool definition.
|
|
29
|
+
*
|
|
30
|
+
* The tool asks the YAGNI app a question grounded in the company's business and
|
|
31
|
+
* codebase context, returning a grounded answer plus source citations. It is the
|
|
32
|
+
* preferred way for the agent to learn anything org- or codebase-specific
|
|
33
|
+
* (conventions, policies, architecture, ownership) before guessing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function makeAskYagniTool(opts: MakeAskYagniToolOptions): ToolDefinition<typeof parameters, {
|
|
36
|
+
citations: Citation[];
|
|
37
|
+
}>;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=askYagniTool.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { collectRepoDocs } from "./repoDocs.js";
|
|
3
|
+
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
4
|
+
const parameters = Type.Object({
|
|
5
|
+
question: Type.String(),
|
|
6
|
+
context: Type.Optional(Type.String()),
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Build the `ask_yagni` tool definition.
|
|
10
|
+
*
|
|
11
|
+
* The tool asks the YAGNI app a question grounded in the company's business and
|
|
12
|
+
* codebase context, returning a grounded answer plus source citations. It is the
|
|
13
|
+
* preferred way for the agent to learn anything org- or codebase-specific
|
|
14
|
+
* (conventions, policies, architecture, ownership) before guessing.
|
|
15
|
+
*/
|
|
16
|
+
export function makeAskYagniTool(opts) {
|
|
17
|
+
return {
|
|
18
|
+
name: "ask_yagni",
|
|
19
|
+
label: "Ask YAGNI",
|
|
20
|
+
description: "Ask the YAGNI app a question grounded in this company's business and codebase context. " +
|
|
21
|
+
"Returns a grounded answer plus source citations. Use this BEFORE guessing about anything " +
|
|
22
|
+
"organization- or codebase-specific — conventions, policies, architecture, ownership, " +
|
|
23
|
+
"product decisions, or who/what/why. Prefer asking over assuming.",
|
|
24
|
+
promptSnippet: "ask_yagni: ask the YAGNI app a question grounded in company + codebase context (returns a cited answer).",
|
|
25
|
+
promptGuidelines: [
|
|
26
|
+
"Call ask_yagni BEFORE guessing about anything organization- or codebase-specific (conventions, policies, architecture, ownership, product decisions).",
|
|
27
|
+
"Pass the user's actual question; add relevant local context (file paths, snippets) in the optional `context` field.",
|
|
28
|
+
"When you use an answer, quote or reference its citations so the user can verify the source.",
|
|
29
|
+
],
|
|
30
|
+
parameters,
|
|
31
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
32
|
+
onUpdate?.({ content: [{ type: "text", text: "Asking YAGNI…" }], details: { citations: [] } });
|
|
33
|
+
// Era-correct repo grounding: gather the working tree's most relevant docs
|
|
34
|
+
// for this question. Never blocking and never fatal — an empty list sends
|
|
35
|
+
// nothing and the backend answers from workspace context alone.
|
|
36
|
+
const collect = opts.collectDocs ?? collectRepoDocs;
|
|
37
|
+
const repoQuery = params.context ? `${params.question}\n${params.context}` : params.question;
|
|
38
|
+
const repoDocs = ctx?.cwd ? collect(ctx.cwd, repoQuery) : [];
|
|
39
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/ask`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"content-type": "application/json",
|
|
43
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
question: params.question,
|
|
47
|
+
context: params.context,
|
|
48
|
+
cwd: ctx?.cwd,
|
|
49
|
+
...(repoDocs.length > 0 ? { repoDocs } : {}),
|
|
50
|
+
}),
|
|
51
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
throw new Error(await friendlyFetchError("ask_yagni", res));
|
|
54
|
+
}
|
|
55
|
+
const data = (await res.json());
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: data.answer }],
|
|
58
|
+
details: { citations: data.citations ?? [] },
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=askYagniTool.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped bless store for review-mode "don't ask again" (spec §4, W4).
|
|
3
|
+
*
|
|
4
|
+
* A bless rule is (tool + directory prefix). It is created ONLY when the user
|
|
5
|
+
* picks "Yes, and don't ask again for <tool> in <dir>" at the review gate, and
|
|
6
|
+
* it is:
|
|
7
|
+
* - SESSION-SCOPED: held in memory for the life of THIS process only.
|
|
8
|
+
* - NEVER persisted (no disk, no backend) — a new session starts unblessed,
|
|
9
|
+
* and (per the gate wiring) it is never consulted in plan mode.
|
|
10
|
+
* - PATH-PREFIX-bound: a rule blesses a tool within one directory subtree, so
|
|
11
|
+
* approving `write` under `src/api/` never auto-approves a `write` elsewhere.
|
|
12
|
+
* - Only for path-bearing tools (read/write/edit expose `path`). `bash` carries
|
|
13
|
+
* no path, so it is NOT blessable and always re-prompts (fail toward asking).
|
|
14
|
+
*
|
|
15
|
+
* Paths are resolved to absolute against the session cwd at BOTH bless-time and
|
|
16
|
+
* match-time, so a rule created from a relative `path` still matches an absolute
|
|
17
|
+
* one (and vice versa). Everything is pure except the in-memory rule list.
|
|
18
|
+
*/
|
|
19
|
+
/** One session bless: this tool is auto-approved within this directory subtree. */
|
|
20
|
+
export interface BlessRule {
|
|
21
|
+
tool: string;
|
|
22
|
+
/** Absolute directory prefix the rule blesses (subtree-inclusive). */
|
|
23
|
+
prefix: string;
|
|
24
|
+
}
|
|
25
|
+
export interface BlessStore {
|
|
26
|
+
/** Can a bless rule be formed for this call? (path-bearing tools only.) */
|
|
27
|
+
canBless(params: Record<string, unknown>): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* A short, cwd-relative directory label for the "don't ask again" option, or
|
|
30
|
+
* null when the call carries no path (bash). Absolute when outside the tree,
|
|
31
|
+
* "." at the repo root, otherwise the relative directory.
|
|
32
|
+
*/
|
|
33
|
+
describeDir(params: Record<string, unknown>): string | null;
|
|
34
|
+
/** Add a (tool + dir-prefix) rule from a blessed call. Null if not blessable. */
|
|
35
|
+
add(tool: string, params: Record<string, unknown>): BlessRule | null;
|
|
36
|
+
/** Does an existing rule already bless this call? */
|
|
37
|
+
isBlessed(tool: string, params: Record<string, unknown>): boolean;
|
|
38
|
+
/** The current rules (test/debug visibility). */
|
|
39
|
+
rules(): BlessRule[];
|
|
40
|
+
}
|
|
41
|
+
/** The file path a call targets, or null for path-less tools (bash). */
|
|
42
|
+
export declare function blessPath(params: Record<string, unknown>): string | null;
|
|
43
|
+
/** Build a fresh, empty session bless store rooted at `cwd`. */
|
|
44
|
+
export declare function makeBlessStore(cwd: string): BlessStore;
|
|
45
|
+
//# sourceMappingURL=bless.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped bless store for review-mode "don't ask again" (spec §4, W4).
|
|
3
|
+
*
|
|
4
|
+
* A bless rule is (tool + directory prefix). It is created ONLY when the user
|
|
5
|
+
* picks "Yes, and don't ask again for <tool> in <dir>" at the review gate, and
|
|
6
|
+
* it is:
|
|
7
|
+
* - SESSION-SCOPED: held in memory for the life of THIS process only.
|
|
8
|
+
* - NEVER persisted (no disk, no backend) — a new session starts unblessed,
|
|
9
|
+
* and (per the gate wiring) it is never consulted in plan mode.
|
|
10
|
+
* - PATH-PREFIX-bound: a rule blesses a tool within one directory subtree, so
|
|
11
|
+
* approving `write` under `src/api/` never auto-approves a `write` elsewhere.
|
|
12
|
+
* - Only for path-bearing tools (read/write/edit expose `path`). `bash` carries
|
|
13
|
+
* no path, so it is NOT blessable and always re-prompts (fail toward asking).
|
|
14
|
+
*
|
|
15
|
+
* Paths are resolved to absolute against the session cwd at BOTH bless-time and
|
|
16
|
+
* match-time, so a rule created from a relative `path` still matches an absolute
|
|
17
|
+
* one (and vice versa). Everything is pure except the in-memory rule list.
|
|
18
|
+
*/
|
|
19
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
20
|
+
/** The file path a call targets, or null for path-less tools (bash). */
|
|
21
|
+
export function blessPath(params) {
|
|
22
|
+
const p = params.path;
|
|
23
|
+
return typeof p === "string" && p.trim().length > 0 ? p.trim() : null;
|
|
24
|
+
}
|
|
25
|
+
/** Build a fresh, empty session bless store rooted at `cwd`. */
|
|
26
|
+
export function makeBlessStore(cwd) {
|
|
27
|
+
const rules = [];
|
|
28
|
+
const abs = (p) => (isAbsolute(p) ? p : resolve(cwd, p));
|
|
29
|
+
/** Absolute directory prefix a bless of this call would cover, or null. */
|
|
30
|
+
function prefixFor(params) {
|
|
31
|
+
const p = blessPath(params);
|
|
32
|
+
if (p === null)
|
|
33
|
+
return null;
|
|
34
|
+
return abs(dirname(p));
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
canBless(params) {
|
|
38
|
+
return blessPath(params) !== null;
|
|
39
|
+
},
|
|
40
|
+
describeDir(params) {
|
|
41
|
+
const prefix = prefixFor(params);
|
|
42
|
+
if (prefix === null)
|
|
43
|
+
return null;
|
|
44
|
+
const rel = relative(cwd, prefix);
|
|
45
|
+
// Inside the tree → the relative dir (or "." for the repo root); outside →
|
|
46
|
+
// the absolute path so the user sees exactly what they are blessing.
|
|
47
|
+
if (rel === "")
|
|
48
|
+
return ".";
|
|
49
|
+
if (!rel.startsWith("..") && !isAbsolute(rel))
|
|
50
|
+
return rel;
|
|
51
|
+
return prefix;
|
|
52
|
+
},
|
|
53
|
+
add(tool, params) {
|
|
54
|
+
const prefix = prefixFor(params);
|
|
55
|
+
if (prefix === null)
|
|
56
|
+
return null;
|
|
57
|
+
if (!rules.some((r) => r.tool === tool && r.prefix === prefix)) {
|
|
58
|
+
rules.push({ tool, prefix });
|
|
59
|
+
}
|
|
60
|
+
return { tool, prefix };
|
|
61
|
+
},
|
|
62
|
+
isBlessed(tool, params) {
|
|
63
|
+
const p = blessPath(params);
|
|
64
|
+
if (p === null)
|
|
65
|
+
return false; // path-less (bash) never auto-approves
|
|
66
|
+
const target = abs(p);
|
|
67
|
+
return rules.some((r) => r.tool === tool && (target === r.prefix || target.startsWith(r.prefix + sep)));
|
|
68
|
+
},
|
|
69
|
+
rules() {
|
|
70
|
+
return rules.slice();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=bless.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAGNI Code branding.
|
|
3
|
+
*
|
|
4
|
+
* YAGNI Code IS the pi coding agent, but a developer running it should never see
|
|
5
|
+
* "pi" or "Earendil" — not in the agent's self-description, not in the chrome.
|
|
6
|
+
* pi stays an unmodified dependency; we rebrand entirely through its extension
|
|
7
|
+
* API. This module owns the one place that matters most: the system prompt
|
|
8
|
+
* identity. It is a pure function so it is fully unit-testable without pi.
|
|
9
|
+
*/
|
|
10
|
+
/** User-facing product name. */
|
|
11
|
+
export declare const BRAND_NAME = "YAGNI Code";
|
|
12
|
+
/**
|
|
13
|
+
* The YAGNI Code identity paragraph that replaces pi's default
|
|
14
|
+
* "You are an expert coding assistant operating inside pi..." opener. It states
|
|
15
|
+
* the differentiator (connected to the YAGNI app) and the working contract
|
|
16
|
+
* (consult ask_yagni before guessing) so the agent behaves more autonomously.
|
|
17
|
+
*/
|
|
18
|
+
export declare const YAGNI_IDENTITY: string;
|
|
19
|
+
export declare const PI_IDENTITY_RE: RegExp;
|
|
20
|
+
export interface BrandSystemPromptOptions {
|
|
21
|
+
/** Override the identity paragraph (defaults to {@link YAGNI_IDENTITY}). */
|
|
22
|
+
identity?: string;
|
|
23
|
+
/** Live company brief to inject so the agent boots grounded (Phase C). */
|
|
24
|
+
contextBrief?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Rebrand pi's assembled system prompt as YAGNI Code's, and optionally inject a
|
|
28
|
+
* live company brief.
|
|
29
|
+
*
|
|
30
|
+
* Steps: (1) drop pi's self-referential documentation block, (2) replace the
|
|
31
|
+
* identity sentence (with a loose fallback, else prepend), (3) scrub residual
|
|
32
|
+
* brand tokens outside user content, (4) append the company brief + a proactive
|
|
33
|
+
* ask_yagni directive. Idempotent: re-branding keeps a single identity.
|
|
34
|
+
*/
|
|
35
|
+
export declare function brandSystemPrompt(original: string, opts?: BrandSystemPromptOptions): string;
|
|
36
|
+
/** The minimal theme surface the masthead needs (a subset of pi's Theme). */
|
|
37
|
+
export interface MastheadTheme {
|
|
38
|
+
bold(s: string): string;
|
|
39
|
+
fg(color: string, s: string): string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build the YAGNI Code startup masthead string, rendered into a header that
|
|
43
|
+
* REPLACES pi's built-in startup banner (which otherwise shows "pi v<version>"
|
|
44
|
+
* and a "Pi can explain its own features…" onboarding line). Kept here as a
|
|
45
|
+
* pure string builder so its content is unit-testable without a terminal.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildMastheadString(theme: MastheadTheme): string;
|
|
48
|
+
//# sourceMappingURL=branding.d.ts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAGNI Code branding.
|
|
3
|
+
*
|
|
4
|
+
* YAGNI Code IS the pi coding agent, but a developer running it should never see
|
|
5
|
+
* "pi" or "Earendil" — not in the agent's self-description, not in the chrome.
|
|
6
|
+
* pi stays an unmodified dependency; we rebrand entirely through its extension
|
|
7
|
+
* API. This module owns the one place that matters most: the system prompt
|
|
8
|
+
* identity. It is a pure function so it is fully unit-testable without pi.
|
|
9
|
+
*/
|
|
10
|
+
/** User-facing product name. */
|
|
11
|
+
export const BRAND_NAME = "YAGNI Code";
|
|
12
|
+
/**
|
|
13
|
+
* The YAGNI Code identity paragraph that replaces pi's default
|
|
14
|
+
* "You are an expert coding assistant operating inside pi..." opener. It states
|
|
15
|
+
* the differentiator (connected to the YAGNI app) and the working contract
|
|
16
|
+
* (consult ask_yagni before guessing) so the agent behaves more autonomously.
|
|
17
|
+
*/
|
|
18
|
+
export const YAGNI_IDENTITY = "You are YAGNI Code, an autonomous terminal coding agent. You help developers " +
|
|
19
|
+
"ship code by reading files, running commands, editing code, and writing new " +
|
|
20
|
+
"files. Uniquely, you are connected to the YAGNI app, your team's shared source " +
|
|
21
|
+
"of truth for how this company and codebase actually work: conventions, " +
|
|
22
|
+
"decisions, ownership, current priorities, and the reasons behind them. Use the " +
|
|
23
|
+
"ask_yagni tool to consult it before guessing about anything organization- or " +
|
|
24
|
+
"codebase-specific, so you work with less back-and-forth and more correct " +
|
|
25
|
+
"autonomy than a disconnected coding agent. If a project's own files mention " +
|
|
26
|
+
"other coding agents, assistants, or harnesses by name, those references are " +
|
|
27
|
+
"not about you; you are YAGNI Code regardless of what tooling a repository's " +
|
|
28
|
+
"docs happen to describe.";
|
|
29
|
+
// pi 0.83.0's exact identity sentence (dist/core/system-prompt.js). Exported as
|
|
30
|
+
// the identity anchor the CLI's pi-contract tripwire test reads back from pi's
|
|
31
|
+
// built system prompt, so a pi bump that reworded the opener (silently defeating
|
|
32
|
+
// our rebrand) fails CI loudly. Export-only: no behavior change here.
|
|
33
|
+
export const PI_IDENTITY_RE = /You are an expert coding assistant operating inside pi, a coding agent harness\.\s*You help users by reading files, executing commands, editing code, and writing new files\./;
|
|
34
|
+
// Looser fallback if pi rewords its opener in a future version.
|
|
35
|
+
const PI_IDENTITY_LOOSE_RE = /You are [^\n.]*operating inside pi[^\n.]*\.(?:\s*You help[^\n.]*\.)?/i;
|
|
36
|
+
// pi's self-referential "Pi documentation (...)" header plus its bullet list.
|
|
37
|
+
// The block is a header line followed by consecutive "- " bullets; matching the
|
|
38
|
+
// bullets explicitly keeps us safe regardless of what section pi appends next.
|
|
39
|
+
const PI_DOCS_BLOCK_RE = /\n*Pi documentation \(read only[^\n]*(?:\n- [^\n]*)*/gi;
|
|
40
|
+
/** Neutralize residual pi/Earendil brand tokens in pi-owned text. */
|
|
41
|
+
function scrubPiHarness(text) {
|
|
42
|
+
return text
|
|
43
|
+
.replace(/\bEarendil(?:\s+Works)?\b/gi, "YAGNI")
|
|
44
|
+
// Whole-word "pi" only, so "api", "pip", "pipeline" are untouched.
|
|
45
|
+
.replace(/\bpi\b/gi, BRAND_NAME);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Scrub pi-brand tokens everywhere EXCEPT inside the user's <project_context>.
|
|
49
|
+
* The user's own AGENTS.md may legitimately mention "pi-extension-yagni"; a
|
|
50
|
+
* blanket scrub would corrupt their instructions. pi wraps project files in
|
|
51
|
+
* <project_context>…</project_context>, so we leave those segments verbatim.
|
|
52
|
+
*/
|
|
53
|
+
function scrubOutsideProjectContext(s) {
|
|
54
|
+
const parts = s.split(/(<project_context>[\s\S]*?<\/project_context>)/);
|
|
55
|
+
return parts
|
|
56
|
+
.map((p) => (p.startsWith("<project_context>") ? p : scrubPiHarness(p)))
|
|
57
|
+
.join("");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Rebrand pi's assembled system prompt as YAGNI Code's, and optionally inject a
|
|
61
|
+
* live company brief.
|
|
62
|
+
*
|
|
63
|
+
* Steps: (1) drop pi's self-referential documentation block, (2) replace the
|
|
64
|
+
* identity sentence (with a loose fallback, else prepend), (3) scrub residual
|
|
65
|
+
* brand tokens outside user content, (4) append the company brief + a proactive
|
|
66
|
+
* ask_yagni directive. Idempotent: re-branding keeps a single identity.
|
|
67
|
+
*/
|
|
68
|
+
export function brandSystemPrompt(original, opts = {}) {
|
|
69
|
+
const identity = opts.identity ?? YAGNI_IDENTITY;
|
|
70
|
+
let s = original;
|
|
71
|
+
// 1. Drop pi's self-referential documentation block.
|
|
72
|
+
s = s.replace(PI_DOCS_BLOCK_RE, "");
|
|
73
|
+
// 2. Own the identity.
|
|
74
|
+
if (PI_IDENTITY_RE.test(s)) {
|
|
75
|
+
s = s.replace(PI_IDENTITY_RE, identity);
|
|
76
|
+
}
|
|
77
|
+
else if (PI_IDENTITY_LOOSE_RE.test(s)) {
|
|
78
|
+
s = s.replace(PI_IDENTITY_LOOSE_RE, identity);
|
|
79
|
+
}
|
|
80
|
+
else if (!s.trimStart().startsWith(identity)) {
|
|
81
|
+
s = `${identity}\n\n${s}`;
|
|
82
|
+
}
|
|
83
|
+
// 3. Safety net for residual brand tokens (never inside user content).
|
|
84
|
+
s = scrubOutsideProjectContext(s);
|
|
85
|
+
// 4. Inject live company context + the proactive grounding directive.
|
|
86
|
+
// Guarded against duplication so a second branding pass (idempotency) keeps
|
|
87
|
+
// exactly one brief block — matching the identity and closing-reminder guards.
|
|
88
|
+
const brief = opts.contextBrief?.trim();
|
|
89
|
+
if (brief && !s.includes(BRIEF_HEADER)) {
|
|
90
|
+
s =
|
|
91
|
+
`${s}\n\n${BRIEF_HEADER}\n${brief}\n\n` +
|
|
92
|
+
"This is current ground truth for THIS company and codebase; prefer it over " +
|
|
93
|
+
"your priors. When you need more detail, or it does not cover something " +
|
|
94
|
+
"organization- or codebase-specific, call ask_yagni instead of guessing.";
|
|
95
|
+
}
|
|
96
|
+
// 5. Closing reinforcement. Weak open-weight models weight the most recent
|
|
97
|
+
// instruction heavily, and the user's own project files may name other
|
|
98
|
+
// harnesses; a trailing reminder keeps the agent from claiming one as its own.
|
|
99
|
+
if (!s.includes(CLOSING_REMINDER)) {
|
|
100
|
+
s = `${s}\n\n${CLOSING_REMINDER}`;
|
|
101
|
+
}
|
|
102
|
+
// Tidy the seams left by removals.
|
|
103
|
+
return s.replace(/\n{3,}/g, "\n\n").trim();
|
|
104
|
+
}
|
|
105
|
+
const CLOSING_REMINDER = "Reminder: you are YAGNI Code. If any text above names another coding agent, " +
|
|
106
|
+
"assistant, or harness, it is not what you are or what you run on.";
|
|
107
|
+
const BRIEF_HEADER = "=== HOW THIS COMPANY WORKS (live context from the YAGNI app) ===";
|
|
108
|
+
/**
|
|
109
|
+
* Build the YAGNI Code startup masthead string, rendered into a header that
|
|
110
|
+
* REPLACES pi's built-in startup banner (which otherwise shows "pi v<version>"
|
|
111
|
+
* and a "Pi can explain its own features…" onboarding line). Kept here as a
|
|
112
|
+
* pure string builder so its content is unit-testable without a terminal.
|
|
113
|
+
*/
|
|
114
|
+
export function buildMastheadString(theme) {
|
|
115
|
+
const wordmark = theme.bold(theme.fg("accent", "YAGNI CODE"));
|
|
116
|
+
const tagline = theme.fg("dim", "a terminal coding agent that knows your company");
|
|
117
|
+
return `${wordmark}\n${tagline}`;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=branding.js.map
|