@yagni-app/code 0.2.0 → 0.3.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.
- package/dist/cli.d.ts +30 -0
- package/dist/cli.js +135 -3
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/extension/advisor.d.ts +4 -4
- package/dist/extension/advisor.js +6 -7
- package/dist/extension/approvedPrefixes.d.ts +92 -0
- package/dist/extension/approvedPrefixes.js +252 -0
- package/dist/extension/askAdvisorTool.d.ts +2 -2
- package/dist/extension/askAdvisorTool.js +5 -5
- package/dist/extension/askYagniTool.js +49 -0
- package/dist/extension/branding.d.ts +24 -3
- package/dist/extension/branding.js +71 -10
- package/dist/extension/chipEditor.d.ts +30 -9
- package/dist/extension/chipEditor.js +173 -59
- package/dist/extension/claudeRules.d.ts +0 -2
- package/dist/extension/claudeRules.js +0 -8
- package/dist/extension/cmux/dispatcher.d.ts +25 -0
- package/dist/extension/cmux/dispatcher.js +266 -0
- package/dist/extension/cmux/hooks.d.ts +12 -0
- package/dist/extension/cmux/hooks.js +192 -0
- package/dist/extension/cmux/index.d.ts +3 -0
- package/dist/extension/cmux/index.js +155 -0
- package/dist/extension/cmux/naming.d.ts +5 -0
- package/dist/extension/cmux/naming.js +23 -0
- package/dist/extension/cmux/state.d.ts +33 -0
- package/dist/extension/cmux/state.js +142 -0
- package/dist/extension/config.d.ts +32 -1
- package/dist/extension/config.js +36 -4
- package/dist/extension/costHud.d.ts +16 -22
- package/dist/extension/costHud.js +8 -47
- package/dist/extension/crashReport.js +1 -3
- package/dist/extension/execPolicy.d.ts +119 -0
- package/dist/extension/execPolicy.js +805 -0
- package/dist/extension/footer.d.ts +111 -0
- package/dist/extension/footer.js +294 -0
- package/dist/extension/guardian.d.ts +129 -0
- package/dist/extension/guardian.js +213 -0
- package/dist/extension/index.d.ts +15 -4
- package/dist/extension/index.js +250 -24
- package/dist/extension/permission.d.ts +123 -10
- package/dist/extension/permission.js +586 -40
- package/dist/extension/pipeline/childRegistry.d.ts +41 -0
- package/dist/extension/pipeline/childRegistry.js +118 -0
- package/dist/extension/pipeline/finish.js +5 -1
- package/dist/extension/pipeline/goCommand.d.ts +1 -1
- package/dist/extension/pipeline/goCommand.js +35 -6
- package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
- package/dist/extension/pipeline/goStatusCommands.js +61 -1
- package/dist/extension/pipeline/personas.js +25 -0
- package/dist/extension/pipeline/runRegistry.d.ts +14 -0
- package/dist/extension/pipeline/runRegistry.js +35 -0
- package/dist/extension/pipeline/runner.js +4 -0
- package/dist/extension/pipeline/verify.d.ts +4 -0
- package/dist/extension/pipeline/verify.js +48 -26
- package/dist/extension/redact.d.ts +20 -0
- package/dist/extension/redact.js +64 -0
- package/dist/extension/rerouteNotice.d.ts +3 -12
- package/dist/extension/rerouteNotice.js +36 -15
- package/dist/extension/subagentRender.d.ts +129 -0
- package/dist/extension/subagentRender.js +441 -0
- package/dist/extension/subagents.d.ts +4 -7
- package/dist/extension/subagents.js +103 -33
- package/dist/extension/ticketTools.d.ts +37 -0
- package/dist/extension/ticketTools.js +117 -0
- package/dist/extension/tokenProvider.js +46 -5
- package/dist/launch.d.ts +7 -0
- package/dist/launch.js +24 -12
- package/dist/padding.d.ts +22 -0
- package/dist/padding.js +25 -0
- package/dist/promptEnrichment.d.ts +40 -0
- package/dist/promptEnrichment.js +85 -0
- package/dist/signalForward.d.ts +60 -0
- package/dist/signalForward.js +130 -0
- package/package.json +5 -5
- package/dist/extension/boostCommand.d.ts +0 -144
- package/dist/extension/boostCommand.js +0 -263
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Guardian — LLM auto-review of prompt-band bash commands (YAG-504).
|
|
3
|
+
*
|
|
4
|
+
* Pure half: verdict types, state tracking, circuit breaker, JSON parsing.
|
|
5
|
+
* I/O half: reviewCommand spawns a locked-down child pi (same runStage seam
|
|
6
|
+
* the advisor uses) on the efficient tier with read-only tools and a risk
|
|
7
|
+
* policy persona. The child returns a JSON verdict; the gate acts on it.
|
|
8
|
+
*
|
|
9
|
+
* Same pure/IO split as advisor.ts (decideConsult pure, askAdvisorTool I/O)
|
|
10
|
+
* and permission.ts (decideGate pure, registerPermissionGate I/O), for the
|
|
11
|
+
* same reason: the rules are what need exhaustive tests, and they must not
|
|
12
|
+
* require a child process to exercise.
|
|
13
|
+
*
|
|
14
|
+
* Trigger: the exec policy classifies a bash command as "prompt" (not clearly
|
|
15
|
+
* safe, not clearly forbidden). The Guardian reviews it instead of interrupting
|
|
16
|
+
* the user. On allow, the command runs. On ask (YAG-510), the user arbitrates:
|
|
17
|
+
* the gate shows the Guardian's question-rationale and the user approves or
|
|
18
|
+
* declines. On deny, the agent sees the rationale and is told to find a safer
|
|
19
|
+
* alternative or ask the user. On timeout/error, auto mode falls back to an
|
|
20
|
+
* ask when a UI exists, else fails closed; review mode falls back to the
|
|
21
|
+
* ordinary user confirm.
|
|
22
|
+
*
|
|
23
|
+
* Circuit breaker: 3 consecutive denials within one user prompt → escalation
|
|
24
|
+
* (ask the user once) or, headless, interruption. Denial streaks reset on
|
|
25
|
+
* `before_agent_start`, which fires once per USER PROMPT (not per LLM turn).
|
|
26
|
+
* An `ask` outcome leaves the streak untouched — neither a denial nor an
|
|
27
|
+
* exoneration — so an ask-preferring model cannot disarm the breaker by
|
|
28
|
+
* alternating deny/ask.
|
|
29
|
+
*/
|
|
30
|
+
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
31
|
+
export const DEFAULT_GUARDIAN_LIMITS = {
|
|
32
|
+
maxReviews: 30,
|
|
33
|
+
maxConsecutiveDenials: 3,
|
|
34
|
+
timeoutMs: 15_000,
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Resolve Guardian limits from the environment. `YAGNI_GUARDIAN_MAX_REVIEWS`
|
|
38
|
+
* overrides the session review cap; anything non-numeric or < 1 falls back to
|
|
39
|
+
* the default (a bad value must never zero out the cap and lock the session).
|
|
40
|
+
*/
|
|
41
|
+
export function resolveGuardianLimits(env = process.env) {
|
|
42
|
+
const raw = env.YAGNI_GUARDIAN_MAX_REVIEWS?.trim();
|
|
43
|
+
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
|
|
44
|
+
const maxReviews = Number.isFinite(parsed) && parsed >= 1 ? parsed : DEFAULT_GUARDIAN_LIMITS.maxReviews;
|
|
45
|
+
return { ...DEFAULT_GUARDIAN_LIMITS, maxReviews };
|
|
46
|
+
}
|
|
47
|
+
/** The model tier the Guardian runs on. Configurable via YAGNI_GUARDIAN_TIER. */
|
|
48
|
+
export const GUARDIAN_MODEL_TIER = "efficient";
|
|
49
|
+
/** Read-only tools — the Guardian can read files for context but cannot write or execute. */
|
|
50
|
+
export const GUARDIAN_TOOLS = ["read"];
|
|
51
|
+
export function makeGuardianState() {
|
|
52
|
+
const state = { reviews: 0, consecutiveDenials: 0 };
|
|
53
|
+
return {
|
|
54
|
+
read: () => ({ ...state }),
|
|
55
|
+
recordReview(outcome) {
|
|
56
|
+
state.reviews += 1;
|
|
57
|
+
if (outcome === "deny") {
|
|
58
|
+
state.consecutiveDenials += 1;
|
|
59
|
+
}
|
|
60
|
+
else if (outcome === "allow") {
|
|
61
|
+
state.consecutiveDenials = 0;
|
|
62
|
+
}
|
|
63
|
+
// "ask" leaves the denial streak UNCHANGED: it is neither a denial nor
|
|
64
|
+
// an exoneration. If it reset the streak, deny/ask/deny/ask would never
|
|
65
|
+
// trip the breaker (round-2 review blocker).
|
|
66
|
+
return { ...state };
|
|
67
|
+
},
|
|
68
|
+
resetTurn() {
|
|
69
|
+
state.consecutiveDenials = 0;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function checkCircuitBreaker(state, limits) {
|
|
74
|
+
if (state.consecutiveDenials >= limits.maxConsecutiveDenials) {
|
|
75
|
+
return {
|
|
76
|
+
tripped: true,
|
|
77
|
+
reason: `Guardian denied ${state.consecutiveDenials} consecutive actions this turn. Pausing for human review.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { tripped: false };
|
|
81
|
+
}
|
|
82
|
+
// --- Verdict parsing (fail closed on malformed) ---
|
|
83
|
+
export function parseVerdict(raw) {
|
|
84
|
+
try {
|
|
85
|
+
// Efficient-tier models may wrap JSON in markdown fences despite
|
|
86
|
+
// instructions to output raw JSON. Extract the first {...} block.
|
|
87
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
88
|
+
const jsonStr = jsonMatch ? jsonMatch[0] : raw;
|
|
89
|
+
const parsed = JSON.parse(jsonStr);
|
|
90
|
+
const outcome = parsed?.outcome;
|
|
91
|
+
if (outcome !== "allow" && outcome !== "ask" && outcome !== "deny")
|
|
92
|
+
return null;
|
|
93
|
+
const riskLevel = parsed.riskLevel;
|
|
94
|
+
const validLevels = ["low", "medium", "high", "critical"];
|
|
95
|
+
return {
|
|
96
|
+
outcome,
|
|
97
|
+
riskLevel: typeof riskLevel === "string" && validLevels.includes(riskLevel)
|
|
98
|
+
? riskLevel
|
|
99
|
+
: "medium",
|
|
100
|
+
rationale: typeof parsed.rationale === "string" && parsed.rationale.trim().length > 0
|
|
101
|
+
? parsed.rationale.trim()
|
|
102
|
+
: "No rationale provided.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// --- /cost subtotal ---
|
|
110
|
+
export function formatGuardianSubtotal(state, limits) {
|
|
111
|
+
if (state.reviews === 0)
|
|
112
|
+
return "";
|
|
113
|
+
const plural = state.reviews === 1 ? "review" : "reviews";
|
|
114
|
+
return `Guardian: ${state.reviews} ${plural}.`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The synthetic stage a Guardian consult runs as. Borrows the `plan` StageId
|
|
118
|
+
* (same pattern as the advisor) so it doesn't ripple into feed/reducers. The
|
|
119
|
+
* agent name selects the guardian persona from PERSONA_BODIES.
|
|
120
|
+
*/
|
|
121
|
+
export function guardianStage(modelTier = GUARDIAN_MODEL_TIER) {
|
|
122
|
+
return {
|
|
123
|
+
id: "plan",
|
|
124
|
+
agent: "guardian",
|
|
125
|
+
model: modelTier,
|
|
126
|
+
tools: GUARDIAN_TOOLS,
|
|
127
|
+
taskTemplate: "{ticket}",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Run a Guardian consult: spawn a locked-down child pi with the risk policy
|
|
132
|
+
* persona and the command as the task. Parse the JSON verdict from the output.
|
|
133
|
+
* Returns { verdict, cost } on success, { verdict: null, error, cost } on failure.
|
|
134
|
+
*/
|
|
135
|
+
export async function reviewCommand(command, deps) {
|
|
136
|
+
const runStage = deps.runStage ?? defaultRunStage;
|
|
137
|
+
const stage = guardianStage(deps.modelTier);
|
|
138
|
+
// Build the abort signal with the Guardian timeout (injectable — the gate
|
|
139
|
+
// threads guardianLimits.timeoutMs through).
|
|
140
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_GUARDIAN_LIMITS.timeoutMs;
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
143
|
+
timer.unref?.();
|
|
144
|
+
// Chain with the caller's signal if present.
|
|
145
|
+
if (deps.signal) {
|
|
146
|
+
if (deps.signal.aborted)
|
|
147
|
+
controller.abort();
|
|
148
|
+
else
|
|
149
|
+
deps.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const justificationLine = deps.execJustification
|
|
153
|
+
? `\n\nWhy this command was routed to you (static policy): ${deps.execJustification}`
|
|
154
|
+
: "";
|
|
155
|
+
const result = await runStage(stage, { ticket: `Review this shell command for safety. Output only the JSON verdict.${justificationLine}\n\nCommand:\n${command}` }, {
|
|
156
|
+
cwd: deps.cwd,
|
|
157
|
+
signal: controller.signal,
|
|
158
|
+
callerLabel: "guardian",
|
|
159
|
+
});
|
|
160
|
+
const cost = result.usage?.cost ?? 0;
|
|
161
|
+
const output = result.finalOutput.trim();
|
|
162
|
+
// An aborted child often RESOLVES with empty/partial output instead of
|
|
163
|
+
// throwing (observed live: ESC → 433ms "empty"). Any result produced
|
|
164
|
+
// while the caller's signal is aborted is an abort, not an outage — the
|
|
165
|
+
// gate must not warn or fall back to an ask on it.
|
|
166
|
+
if (deps.signal?.aborted) {
|
|
167
|
+
return { verdict: null, error: "aborted", cost };
|
|
168
|
+
}
|
|
169
|
+
if (!output) {
|
|
170
|
+
return { verdict: null, error: "empty", cost };
|
|
171
|
+
}
|
|
172
|
+
const verdict = parseVerdict(output);
|
|
173
|
+
if (!verdict) {
|
|
174
|
+
return { verdict: null, error: "malformed", cost };
|
|
175
|
+
}
|
|
176
|
+
return { verdict, cost };
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
// Distinguish the caller aborting (user hit ESC — must NOT be treated as
|
|
180
|
+
// an outage, or the fallback ask would pop a dialog on an aborted turn),
|
|
181
|
+
// our own timeout, and real network/process errors.
|
|
182
|
+
if (deps.signal?.aborted) {
|
|
183
|
+
return { verdict: null, error: "aborted", cost: 0 };
|
|
184
|
+
}
|
|
185
|
+
if (controller.signal.aborted) {
|
|
186
|
+
return { verdict: null, error: "timeout", cost: 0 };
|
|
187
|
+
}
|
|
188
|
+
return { verdict: null, error: "network", cost: 0 };
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Create a sanitized diagnostic event. Never includes the raw command text
|
|
196
|
+
* (could contain secrets). YAGNI_DEBUG=1 adds rationale and a command hash.
|
|
197
|
+
*/
|
|
198
|
+
export function buildDiagnosticEvent(outcome, opts) {
|
|
199
|
+
const ev = {
|
|
200
|
+
event: "guardian_review",
|
|
201
|
+
outcome,
|
|
202
|
+
...(opts.durationMs !== undefined ? { durationMs: opts.durationMs } : {}),
|
|
203
|
+
...(opts.tier !== undefined ? { tier: opts.tier } : {}),
|
|
204
|
+
};
|
|
205
|
+
if (opts.debug) {
|
|
206
|
+
if (opts.rationale)
|
|
207
|
+
ev.rationale = opts.rationale;
|
|
208
|
+
if (opts.commandHash)
|
|
209
|
+
ev.commandHash = opts.commandHash;
|
|
210
|
+
}
|
|
211
|
+
return ev;
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=guardian.js.map
|
|
@@ -4,7 +4,7 @@ import { runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
|
4
4
|
import { fetchMcpServers as defaultFetchMcpServers } from "./mcpTools.js";
|
|
5
5
|
import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
|
|
6
6
|
import { type TokenProvider } from "./tokenProvider.js";
|
|
7
|
-
import { type
|
|
7
|
+
import { type CatalogResult, type ContextBrief } from "./config.js";
|
|
8
8
|
/**
|
|
9
9
|
* YAGNI Code extension entry point.
|
|
10
10
|
*
|
|
@@ -31,7 +31,7 @@ export interface RegisterYagniDeps {
|
|
|
31
31
|
baseUrl: string;
|
|
32
32
|
getToken: () => string | undefined;
|
|
33
33
|
fetchImpl?: typeof fetch;
|
|
34
|
-
}) => Promise<
|
|
34
|
+
}) => Promise<CatalogResult>;
|
|
35
35
|
fetchContextBrief?: (opts: {
|
|
36
36
|
baseUrl: string;
|
|
37
37
|
getToken: () => string | undefined;
|
|
@@ -63,6 +63,12 @@ export interface RegisterYagniDeps {
|
|
|
63
63
|
tokenProvider?: TokenProvider;
|
|
64
64
|
/** The spool flush (R4 write half), injectable so tests never touch disk. */
|
|
65
65
|
flushSpool?: (opts: SpoolClientOpts) => Promise<FlushOutcome>;
|
|
66
|
+
/**
|
|
67
|
+
* Non-fatal auth-event reporter (YAG-500 Fix E). Defaults to
|
|
68
|
+
* `makeCrashReporter` gated on `!evalMode`; inject a spy in tests to assert
|
|
69
|
+
* the report is fired with `context: "auth-failure"` and the refresh outcome.
|
|
70
|
+
*/
|
|
71
|
+
authReporter?: (error: unknown, context?: string) => Promise<void>;
|
|
66
72
|
env?: NodeJS.ProcessEnv;
|
|
67
73
|
}
|
|
68
74
|
/**
|
|
@@ -97,8 +103,11 @@ export declare function parseSpendResponse(data: unknown): SpendResponse | null;
|
|
|
97
103
|
export declare function registerYagni(pi: ExtensionAPI, deps?: RegisterYagniDeps): Promise<void>;
|
|
98
104
|
export default function (pi: ExtensionAPI): Promise<void>;
|
|
99
105
|
export { makeAskYagniTool } from "./askYagniTool.js";
|
|
106
|
+
export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
100
107
|
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
101
108
|
export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
|
|
109
|
+
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, } from "./guardian.js";
|
|
110
|
+
export type { GuardianOutcome, GuardianVerdict, GuardianState, GuardianStateHandle, GuardianLimits, ReviewResult, ReviewCommandDeps, } from "./guardian.js";
|
|
102
111
|
export type { Citation, MakeAskYagniToolOptions } from "./askYagniTool.js";
|
|
103
112
|
export { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
104
113
|
export type { MakeReviewToolOptions } from "./reviewTool.js";
|
|
@@ -115,7 +124,7 @@ export type { RunInitPassDeps, InitPassOutcome, RunTeamSetupDeps, TeamSetupOutco
|
|
|
115
124
|
export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.js";
|
|
116
125
|
export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, BRAND_NAME } from "./branding.js";
|
|
117
126
|
export { attributionHeaders, isDriverCaller, fetchCatalog, getToken, getWorkspaceId, resolveBaseUrl, sanitizeCallerSegment, } from "./config.js";
|
|
118
|
-
export type { FetchCatalogOptions, ModelEntry } from "./config.js";
|
|
127
|
+
export type { CatalogResult, FetchCatalogOptions, ModelEntry } from "./config.js";
|
|
119
128
|
export { buildYagniProvider } from "./provider.js";
|
|
120
129
|
export { registerGoCommand } from "./pipeline/goCommand.js";
|
|
121
130
|
export type { RegisterGoDeps } from "./pipeline/goCommand.js";
|
|
@@ -134,7 +143,9 @@ export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkd
|
|
|
134
143
|
export type { SubagentDef, SubagentSource } from "./subagents.js";
|
|
135
144
|
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, todoSummary, TODO_TOOL_NAME, MAX_TODOS, } from "./todos.js";
|
|
136
145
|
export type { TodoItem, TodoStatus, TodoTheme } from "./todos.js";
|
|
137
|
-
export { decideGate, registerPermissionGate, filterStalePlanContext, DEFAULT_PERMISSION_POLICY, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
|
|
146
|
+
export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
|
|
147
|
+
export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./execPolicy.js";
|
|
148
|
+
export type { ExecDecision, ExecPolicy, PrefixRule, ExecClassification, } from "./execPolicy.js";
|
|
138
149
|
export type { PermissionMode, PermissionPolicy, GateDecision, RegisterPermissionDeps, BlessRememberInfo, } from "./permission.js";
|
|
139
150
|
export { makeBlessStore, blessPath } from "./bless.js";
|
|
140
151
|
export type { BlessStore, BlessRule } from "./bless.js";
|