@yagni-app/code 0.1.0 → 0.2.0

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.
Files changed (155) hide show
  1. package/README.md +65 -11
  2. package/dist/claudeCompat.d.ts +109 -0
  3. package/dist/claudeCompat.js +260 -0
  4. package/dist/claudePlugins.d.ts +109 -0
  5. package/dist/claudePlugins.js +336 -0
  6. package/dist/cli.d.ts +1 -5
  7. package/dist/cli.js +79 -20
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +4 -2
  10. package/dist/crashReport.d.ts +135 -0
  11. package/dist/crashReport.js +291 -0
  12. package/dist/credentials.js +2 -1
  13. package/dist/distribution.d.ts +15 -0
  14. package/dist/distribution.js +45 -0
  15. package/dist/doctor.d.ts +27 -0
  16. package/dist/doctor.js +81 -3
  17. package/dist/extension/advisor.d.ts +84 -0
  18. package/dist/extension/advisor.js +101 -0
  19. package/dist/extension/askAdvisorTool.d.ts +83 -0
  20. package/dist/extension/askAdvisorTool.js +194 -0
  21. package/dist/extension/askYagniTool.d.ts +39 -0
  22. package/dist/extension/askYagniTool.js +63 -0
  23. package/dist/extension/bless.d.ts +45 -0
  24. package/dist/extension/bless.js +87 -0
  25. package/dist/extension/boostCommand.d.ts +144 -0
  26. package/dist/extension/boostCommand.js +263 -0
  27. package/dist/extension/branding.d.ts +79 -0
  28. package/dist/extension/branding.js +156 -0
  29. package/dist/extension/chipEditor.d.ts +117 -0
  30. package/dist/extension/chipEditor.js +373 -0
  31. package/dist/extension/claudeRules.d.ts +54 -0
  32. package/dist/extension/claudeRules.js +180 -0
  33. package/dist/extension/config.d.ts +173 -0
  34. package/dist/extension/config.js +194 -0
  35. package/dist/extension/costHud.d.ts +186 -0
  36. package/dist/extension/costHud.js +293 -0
  37. package/dist/extension/crashReport.d.ts +89 -0
  38. package/dist/extension/crashReport.js +241 -0
  39. package/dist/extension/decisionCapture.d.ts +52 -0
  40. package/dist/extension/decisionCapture.js +66 -0
  41. package/dist/extension/decisions.d.ts +83 -0
  42. package/dist/extension/decisions.js +200 -0
  43. package/dist/extension/diagnostics.d.ts +41 -0
  44. package/dist/extension/diagnostics.js +110 -0
  45. package/dist/extension/index.d.ts +155 -0
  46. package/dist/extension/index.js +597 -0
  47. package/dist/extension/initDone.d.ts +28 -0
  48. package/dist/extension/initDone.js +66 -0
  49. package/dist/extension/initPass.d.ts +170 -0
  50. package/dist/extension/initPass.js +394 -0
  51. package/dist/extension/mcpTools.d.ts +57 -0
  52. package/dist/extension/mcpTools.js +132 -0
  53. package/dist/extension/nextWorkTool.d.ts +51 -0
  54. package/dist/extension/nextWorkTool.js +80 -0
  55. package/dist/extension/permission.d.ts +91 -0
  56. package/dist/extension/permission.js +236 -0
  57. package/dist/extension/pipeline/activity.d.ts +37 -0
  58. package/dist/extension/pipeline/activity.js +151 -0
  59. package/dist/extension/pipeline/activityFeed.d.ts +59 -0
  60. package/dist/extension/pipeline/activityFeed.js +175 -0
  61. package/dist/extension/pipeline/budget.d.ts +48 -0
  62. package/dist/extension/pipeline/budget.js +68 -0
  63. package/dist/extension/pipeline/checkpoint.d.ts +31 -0
  64. package/dist/extension/pipeline/checkpoint.js +176 -0
  65. package/dist/extension/pipeline/eval.d.ts +205 -0
  66. package/dist/extension/pipeline/eval.js +226 -0
  67. package/dist/extension/pipeline/events.d.ts +56 -0
  68. package/dist/extension/pipeline/events.js +147 -0
  69. package/dist/extension/pipeline/findings.d.ts +42 -0
  70. package/dist/extension/pipeline/findings.js +144 -0
  71. package/dist/extension/pipeline/finish.d.ts +128 -0
  72. package/dist/extension/pipeline/finish.js +307 -0
  73. package/dist/extension/pipeline/goCommand.d.ts +146 -0
  74. package/dist/extension/pipeline/goCommand.js +1085 -0
  75. package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
  76. package/dist/extension/pipeline/goCompareCommand.js +203 -0
  77. package/dist/extension/pipeline/goFlags.d.ts +40 -0
  78. package/dist/extension/pipeline/goFlags.js +46 -0
  79. package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
  80. package/dist/extension/pipeline/goStatusCommands.js +188 -0
  81. package/dist/extension/pipeline/invocation.d.ts +45 -0
  82. package/dist/extension/pipeline/invocation.js +64 -0
  83. package/dist/extension/pipeline/orchestrator.d.ts +131 -0
  84. package/dist/extension/pipeline/orchestrator.js +645 -0
  85. package/dist/extension/pipeline/personas.d.ts +44 -0
  86. package/dist/extension/pipeline/personas.js +248 -0
  87. package/dist/extension/pipeline/resilience.d.ts +85 -0
  88. package/dist/extension/pipeline/resilience.js +166 -0
  89. package/dist/extension/pipeline/resume.d.ts +18 -0
  90. package/dist/extension/pipeline/resume.js +106 -0
  91. package/dist/extension/pipeline/runCostTable.d.ts +37 -0
  92. package/dist/extension/pipeline/runCostTable.js +165 -0
  93. package/dist/extension/pipeline/runRegistry.d.ts +112 -0
  94. package/dist/extension/pipeline/runRegistry.js +202 -0
  95. package/dist/extension/pipeline/runSession.d.ts +152 -0
  96. package/dist/extension/pipeline/runSession.js +167 -0
  97. package/dist/extension/pipeline/runState.d.ts +177 -0
  98. package/dist/extension/pipeline/runState.js +275 -0
  99. package/dist/extension/pipeline/runner.d.ts +98 -0
  100. package/dist/extension/pipeline/runner.js +310 -0
  101. package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
  102. package/dist/extension/pipeline/scrubSecrets.js +32 -0
  103. package/dist/extension/pipeline/stages.d.ts +53 -0
  104. package/dist/extension/pipeline/stages.js +115 -0
  105. package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
  106. package/dist/extension/pipeline/ticketResolution.js +75 -0
  107. package/dist/extension/pipeline/types.d.ts +439 -0
  108. package/dist/extension/pipeline/types.js +48 -0
  109. package/dist/extension/pipeline/verify.d.ts +277 -0
  110. package/dist/extension/pipeline/verify.js +763 -0
  111. package/dist/extension/pipeline/workspace.d.ts +32 -0
  112. package/dist/extension/pipeline/workspace.js +73 -0
  113. package/dist/extension/pipeline/worktree.d.ts +107 -0
  114. package/dist/extension/pipeline/worktree.js +202 -0
  115. package/dist/extension/provider.d.ts +17 -0
  116. package/dist/extension/provider.js +33 -0
  117. package/dist/extension/recall.d.ts +93 -0
  118. package/dist/extension/recall.js +190 -0
  119. package/dist/extension/recordContextTool.d.ts +38 -0
  120. package/dist/extension/recordContextTool.js +85 -0
  121. package/dist/extension/recordDecisionTool.d.ts +52 -0
  122. package/dist/extension/recordDecisionTool.js +102 -0
  123. package/dist/extension/repoDocs.d.ts +81 -0
  124. package/dist/extension/repoDocs.js +260 -0
  125. package/dist/extension/rerouteNotice.d.ts +42 -0
  126. package/dist/extension/rerouteNotice.js +67 -0
  127. package/dist/extension/resilientFetch.d.ts +60 -0
  128. package/dist/extension/resilientFetch.js +133 -0
  129. package/dist/extension/reviewTool.d.ts +34 -0
  130. package/dist/extension/reviewTool.js +81 -0
  131. package/dist/extension/sessionRuns.d.ts +45 -0
  132. package/dist/extension/sessionRuns.js +77 -0
  133. package/dist/extension/spool.d.ts +92 -0
  134. package/dist/extension/spool.js +266 -0
  135. package/dist/extension/stateHome.d.ts +2 -0
  136. package/dist/extension/stateHome.js +6 -0
  137. package/dist/extension/subagents.d.ts +145 -0
  138. package/dist/extension/subagents.js +326 -0
  139. package/dist/extension/surface.d.ts +10 -0
  140. package/dist/extension/surface.js +12 -0
  141. package/dist/extension/todos.d.ts +110 -0
  142. package/dist/extension/todos.js +217 -0
  143. package/dist/extension/tokenProvider.d.ts +93 -0
  144. package/dist/extension/tokenProvider.js +234 -0
  145. package/dist/launch.d.ts +25 -3
  146. package/dist/launch.js +27 -9
  147. package/dist/login.d.ts +7 -0
  148. package/dist/login.js +3 -1
  149. package/dist/paths.d.ts +13 -4
  150. package/dist/paths.js +17 -5
  151. package/dist/profiles.d.ts +1 -1
  152. package/dist/profiles.js +5 -2
  153. package/dist/upgrade.d.ts +97 -0
  154. package/dist/upgrade.js +284 -0
  155. 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