@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,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PURE half of the ask_advisor escalation (YAG-380).
|
|
3
|
+
*
|
|
4
|
+
* Balanced sessions drive on `advanced` and escalate hard judgment calls to
|
|
5
|
+
* `peak`. This module owns every decision about whether a consult may happen and
|
|
6
|
+
* what it cost; `askAdvisorTool.ts` owns the spawn. Same split as `permission.ts`
|
|
7
|
+
* (`decideGate` pure, wiring separate), for the same reason: the rules are what
|
|
8
|
+
* need exhaustive tests, and they must not require a child process to exercise.
|
|
9
|
+
*
|
|
10
|
+
* Two bounds, both necessarily POST-HOC. pi exposes no pre-emptive turn or token
|
|
11
|
+
* ceiling (see `pipeline/budget.ts`), so a consult's cost is only knowable once
|
|
12
|
+
* the child has exited:
|
|
13
|
+
*
|
|
14
|
+
* - `maxConsults` is the real product bound. It makes escalation predictable
|
|
15
|
+
* and priceable, which is the point of the cap.
|
|
16
|
+
* - `maxSpend` is a RUNAWAY CATCH, not a budget — deliberately generous, in
|
|
17
|
+
* the same spirit as `DEFAULT_RUN_BUDGET`. A thorough consult on a large
|
|
18
|
+
* repo runs $0.40-$1.00, so a full 5-consult session lands near $2-$5 and
|
|
19
|
+
* never approaches this. It exists only so an advisor stuck in a loop has
|
|
20
|
+
* something to stop it.
|
|
21
|
+
*
|
|
22
|
+
* State is held in a closure via {@link makeAdvisorState}; no module-level
|
|
23
|
+
* mutable state, so two sessions in one process cannot bleed into each other.
|
|
24
|
+
*/
|
|
25
|
+
/** The session tier that may escalate. Balanced only, by design. */
|
|
26
|
+
export const ADVISOR_TIER = "balanced";
|
|
27
|
+
/** The tier a consult itself runs on. */
|
|
28
|
+
export const ADVISOR_MODEL_TIER = "peak";
|
|
29
|
+
export const DEFAULT_ADVISOR_LIMITS = {
|
|
30
|
+
maxConsults: 5,
|
|
31
|
+
maxSpend: 10,
|
|
32
|
+
};
|
|
33
|
+
export function makeAdvisorState() {
|
|
34
|
+
const state = { consults: 0, spend: 0 };
|
|
35
|
+
return {
|
|
36
|
+
read: () => ({ ...state }),
|
|
37
|
+
record(cost) {
|
|
38
|
+
state.consults += 1;
|
|
39
|
+
// A child that reports no usage still counts as a consult — the cap is
|
|
40
|
+
// about escalation attempts, not only about money.
|
|
41
|
+
state.spend += Number.isFinite(cost) && cost > 0 ? cost : 0;
|
|
42
|
+
return { ...state };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Decide whether one consult may proceed. PURE.
|
|
48
|
+
*
|
|
49
|
+
* The tier check reads the model at CALL time rather than at registration time:
|
|
50
|
+
* tools register once at activation but pi's picker can switch the session model
|
|
51
|
+
* afterwards, so a registration-time check would both leave the tool live after
|
|
52
|
+
* switching away from Balanced and hide it forever from a session that switched
|
|
53
|
+
* to Balanced.
|
|
54
|
+
*/
|
|
55
|
+
export function decideConsult(input) {
|
|
56
|
+
const { model, state, limits } = input;
|
|
57
|
+
if (model !== ADVISOR_TIER) {
|
|
58
|
+
return {
|
|
59
|
+
allow: false,
|
|
60
|
+
reason: `ask_advisor is only available on the Balanced tier (this session is on ` +
|
|
61
|
+
`"${model ?? "unknown"}"). Switch the model to Balanced to consult the ` +
|
|
62
|
+
`peak-tier advisor, or reason it through on the current tier.`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (state.consults >= limits.maxConsults) {
|
|
66
|
+
return {
|
|
67
|
+
allow: false,
|
|
68
|
+
reason: `Advisor consult cap reached (${state.consults} of ${limits.maxConsults} ` +
|
|
69
|
+
`used this session). Escalation is capped so a session's cost stays ` +
|
|
70
|
+
`predictable. Decide this one yourself, or start a fresh session.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (state.spend >= limits.maxSpend) {
|
|
74
|
+
return {
|
|
75
|
+
allow: false,
|
|
76
|
+
reason: `Advisor spend ceiling reached ($${state.spend.toFixed(2)} this session). ` +
|
|
77
|
+
`This is a runaway guard rather than a budget, so hitting it usually ` +
|
|
78
|
+
`means a consult looped. Decide this one yourself, or start a fresh session.`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { allow: true };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The line appended to a consult's advice so spend is visible at the moment it
|
|
85
|
+
* happens. `/cost` cannot see child processes at all (it accumulates off the
|
|
86
|
+
* parent's `turn_end`), so without this an advisor consult is invisible — see
|
|
87
|
+
* YAG-383 for the server-authoritative fix.
|
|
88
|
+
*/
|
|
89
|
+
export function formatConsultCost(input) {
|
|
90
|
+
const { cost, consults, limits } = input;
|
|
91
|
+
return `Advisor consult: $${cost.toFixed(2)} (${consults} of ${limits.maxConsults} used this session).`;
|
|
92
|
+
}
|
|
93
|
+
/** The advisor subtotal line `/cost` appends when a session has escalated. */
|
|
94
|
+
export function formatAdvisorSubtotal(state, limits) {
|
|
95
|
+
if (state.consults === 0)
|
|
96
|
+
return "";
|
|
97
|
+
const plural = state.consults === 1 ? "consult" : "consults";
|
|
98
|
+
return (`Advisor: ${state.consults} ${plural} of ${limits.maxConsults}, ` +
|
|
99
|
+
`$${state.spend.toFixed(2)} on peak.`);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=advisor.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
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 { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { Type } from "typebox";
|
|
30
|
+
import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
|
|
31
|
+
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
32
|
+
import type { PipelineStage } from "./pipeline/types.js";
|
|
33
|
+
/**
|
|
34
|
+
* Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
|
|
35
|
+
* reason documented there: enough tools to navigate instead of flailing on a
|
|
36
|
+
* single `read`. No `bash`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const ADVISOR_TOOLS: string[];
|
|
39
|
+
declare const parameters: Type.TObject<{
|
|
40
|
+
question: Type.TString;
|
|
41
|
+
tried: Type.TOptional<Type.TString>;
|
|
42
|
+
excerpts: Type.TOptional<Type.TString>;
|
|
43
|
+
}>;
|
|
44
|
+
export interface MakeAskAdvisorToolOptions {
|
|
45
|
+
/** Per-session consult accounting. Created once per session in index.ts. */
|
|
46
|
+
state: AdvisorStateHandle;
|
|
47
|
+
limits?: AdvisorLimits;
|
|
48
|
+
/** Injectable so tests never spawn a child. */
|
|
49
|
+
runStage?: typeof defaultRunStage;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Assemble the consult brief. The advisor's persona already tells it not to take
|
|
53
|
+
* this framing on trust, so the brief orients rather than constrains.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildConsultBrief(params: {
|
|
56
|
+
question: string;
|
|
57
|
+
tried?: string;
|
|
58
|
+
excerpts?: string;
|
|
59
|
+
}): string;
|
|
60
|
+
/**
|
|
61
|
+
* The synthetic stage a consult runs as.
|
|
62
|
+
*
|
|
63
|
+
* `id` reuses the existing `plan` StageId rather than adding an advisor member:
|
|
64
|
+
* the advisor is NOT a pipeline stage, and a new StageId would ripple into the
|
|
65
|
+
* feed/activity reducers that switch over it. `agent` is what selects the
|
|
66
|
+
* persona, and that is the advisor's own.
|
|
67
|
+
*/
|
|
68
|
+
export declare function advisorStage(): PipelineStage;
|
|
69
|
+
export declare function makeAskAdvisorTool(opts: MakeAskAdvisorToolOptions): ToolDefinition<typeof parameters, {
|
|
70
|
+
consults: number;
|
|
71
|
+
cost: number;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* `/advise <question>` — the manual escalation lever.
|
|
75
|
+
*
|
|
76
|
+
* Runs the SAME tool the model calls, sharing one state handle, so a manual
|
|
77
|
+
* consult draws on the same cap rather than opening a side channel around it.
|
|
78
|
+
* The advice is sent into the conversation (like /go-compare's report) so the
|
|
79
|
+
* driver sees it in context and can act on it.
|
|
80
|
+
*/
|
|
81
|
+
export declare function registerAdviseCommand(pi: ExtensionAPI, tool: ReturnType<typeof makeAskAdvisorTool>): void;
|
|
82
|
+
export {};
|
|
83
|
+
//# sourceMappingURL=askAdvisorTool.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
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) }, {
|
|
117
|
+
cwd: ctx?.cwd ?? process.cwd(),
|
|
118
|
+
...(signal ? { signal } : {}),
|
|
119
|
+
// YAG-471: attribute the consult's completions to the advisor, not
|
|
120
|
+
// the "plan" stage id advisorStage() borrows (see its docblock).
|
|
121
|
+
callerLabel: "advisor",
|
|
122
|
+
});
|
|
123
|
+
const cost = result.usage?.cost ?? 0;
|
|
124
|
+
const state = opts.state.record(cost);
|
|
125
|
+
if (result.exitCode !== 0 && !result.finalOutput.trim()) {
|
|
126
|
+
// Fail honestly rather than returning an empty recommendation. The
|
|
127
|
+
// consult still counts: it spawned, and it may well have spent.
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: `The advisor consult failed (exit ${result.exitCode}). ` +
|
|
133
|
+
`${formatConsultCost({ cost, consults: state.consults, limits })} ` +
|
|
134
|
+
`Decide this one yourself.`,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
details: { consults: state.consults, cost },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: `${result.finalOutput.trim()}\n\n${formatConsultCost({ cost, consults: state.consults, limits })}`,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
details: { consults: state.consults, cost },
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* `/advise <question>` — the manual escalation lever.
|
|
154
|
+
*
|
|
155
|
+
* Runs the SAME tool the model calls, sharing one state handle, so a manual
|
|
156
|
+
* consult draws on the same cap rather than opening a side channel around it.
|
|
157
|
+
* The advice is sent into the conversation (like /go-compare's report) so the
|
|
158
|
+
* driver sees it in context and can act on it.
|
|
159
|
+
*/
|
|
160
|
+
export function registerAdviseCommand(pi, tool) {
|
|
161
|
+
pi.registerCommand("advise", {
|
|
162
|
+
description: "Escalate one hard call to the peak-tier advisor (Balanced sessions only, capped per session).",
|
|
163
|
+
handler: async (args, ctx) => {
|
|
164
|
+
const notify = (message, type) => {
|
|
165
|
+
if (ctx.hasUI)
|
|
166
|
+
ctx.ui.notify(message, type);
|
|
167
|
+
};
|
|
168
|
+
const question = args.trim();
|
|
169
|
+
if (!question) {
|
|
170
|
+
notify("Usage: /advise <the judgment call you want a second opinion on>", "warning");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!ctx.isIdle()) {
|
|
174
|
+
notify("YAGNI Code is busy; wait for the current turn to finish before /advise.", "warning");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
notify("Consulting the peak-tier advisor. This reads the repo, so it takes a moment.", "info");
|
|
178
|
+
try {
|
|
179
|
+
const out = await tool.execute("advise", { question }, ctx.signal, undefined, ctx);
|
|
180
|
+
const text = out.content
|
|
181
|
+
.map((c) => c.text ?? "")
|
|
182
|
+
.join("\n")
|
|
183
|
+
.trim();
|
|
184
|
+
await pi.sendUserMessage(text || "The advisor returned nothing.");
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
+
notify(`/advise failed: ${message}`, "error");
|
|
189
|
+
await pi.sendUserMessage(`/advise failed: ${message}`);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
//# 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,87 @@
|
|
|
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
|
+
/**
|
|
21
|
+
* Windows-only separator normalization so prefixes compare and display with
|
|
22
|
+
* `/` on every platform: pi's tools emit forward-slash paths even on Windows,
|
|
23
|
+
* and a rule keyed `C:\repo\src\api` would silently never match a call for
|
|
24
|
+
* `C:/repo/src/api/a.ts`. On POSIX this is the identity (a `\` there is a
|
|
25
|
+
* legal filename character, not a separator).
|
|
26
|
+
*/
|
|
27
|
+
const norm = sep === "\\" ? (p) => p.split("\\").join("/") : (p) => p;
|
|
28
|
+
/** The file path a call targets, or null for path-less tools (bash). */
|
|
29
|
+
export function blessPath(params) {
|
|
30
|
+
const p = params.path;
|
|
31
|
+
return typeof p === "string" && p.trim().length > 0 ? p.trim() : null;
|
|
32
|
+
}
|
|
33
|
+
/** Build a fresh, empty session bless store rooted at `cwd`. */
|
|
34
|
+
export function makeBlessStore(cwd) {
|
|
35
|
+
const rules = [];
|
|
36
|
+
// Always THROUGH resolve, even for absolute inputs: on Windows a bare
|
|
37
|
+
// "/repo/…" is drive-relative and resolve() drive-qualifies it, so a rule
|
|
38
|
+
// minted from a relative path and a call carrying an absolute one land on
|
|
39
|
+
// the same canonical form. (For an already-absolute POSIX path this is just
|
|
40
|
+
// normalization.)
|
|
41
|
+
const abs = (p) => norm(resolve(cwd, p));
|
|
42
|
+
/** Absolute directory prefix a bless of this call would cover, or null. */
|
|
43
|
+
function prefixFor(params) {
|
|
44
|
+
const p = blessPath(params);
|
|
45
|
+
if (p === null)
|
|
46
|
+
return null;
|
|
47
|
+
return abs(dirname(p));
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
canBless(params) {
|
|
51
|
+
return blessPath(params) !== null;
|
|
52
|
+
},
|
|
53
|
+
describeDir(params) {
|
|
54
|
+
const prefix = prefixFor(params);
|
|
55
|
+
if (prefix === null)
|
|
56
|
+
return null;
|
|
57
|
+
const rel = norm(relative(cwd, prefix));
|
|
58
|
+
// Inside the tree → the relative dir (or "." for the repo root); outside →
|
|
59
|
+
// the absolute path so the user sees exactly what they are blessing.
|
|
60
|
+
if (rel === "")
|
|
61
|
+
return ".";
|
|
62
|
+
if (!rel.startsWith("..") && !isAbsolute(rel))
|
|
63
|
+
return rel;
|
|
64
|
+
return prefix;
|
|
65
|
+
},
|
|
66
|
+
add(tool, params) {
|
|
67
|
+
const prefix = prefixFor(params);
|
|
68
|
+
if (prefix === null)
|
|
69
|
+
return null;
|
|
70
|
+
if (!rules.some((r) => r.tool === tool && r.prefix === prefix)) {
|
|
71
|
+
rules.push({ tool, prefix });
|
|
72
|
+
}
|
|
73
|
+
return { tool, prefix };
|
|
74
|
+
},
|
|
75
|
+
isBlessed(tool, params) {
|
|
76
|
+
const p = blessPath(params);
|
|
77
|
+
if (p === null)
|
|
78
|
+
return false; // path-less (bash) never auto-approves
|
|
79
|
+
const target = abs(p);
|
|
80
|
+
return rules.some((r) => r.tool === tool && (target === r.prefix || target.startsWith(`${r.prefix}/`)));
|
|
81
|
+
},
|
|
82
|
+
rules() {
|
|
83
|
+
return rules.slice();
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=bless.js.map
|