@yagni-app/code-staging 0.3.0-staging.1079.1 → 0.3.0-staging.1081.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/dist/extension/approvedPrefixes.d.ts +92 -0
- package/dist/extension/approvedPrefixes.js +252 -0
- package/dist/extension/config.d.ts +21 -0
- package/dist/extension/config.js +36 -2
- package/dist/extension/execPolicy.d.ts +51 -13
- package/dist/extension/execPolicy.js +432 -80
- package/dist/extension/guardian.d.ts +22 -6
- package/dist/extension/guardian.js +38 -11
- package/dist/extension/index.js +77 -10
- package/dist/extension/permission.d.ts +55 -0
- package/dist/extension/permission.js +395 -100
- package/dist/extension/pipeline/personas.js +12 -9
- package/dist/extension/redact.d.ts +20 -0
- package/dist/extension/redact.js +64 -0
- package/package.json +2 -2
|
@@ -13,15 +13,23 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Trigger: the exec policy classifies a bash command as "prompt" (not clearly
|
|
15
15
|
* safe, not clearly forbidden). The Guardian reviews it instead of interrupting
|
|
16
|
-
* the user. On allow, the command runs. On
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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.
|
|
19
22
|
*
|
|
20
|
-
* Circuit breaker: 3 consecutive denials
|
|
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.
|
|
21
29
|
*/
|
|
22
30
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
23
31
|
import type { PipelineStage } from "./pipeline/types.js";
|
|
24
|
-
export type GuardianOutcome = "allow" | "deny";
|
|
32
|
+
export type GuardianOutcome = "allow" | "ask" | "deny";
|
|
25
33
|
export type GuardianRiskLevel = "low" | "medium" | "high" | "critical";
|
|
26
34
|
export interface GuardianVerdict {
|
|
27
35
|
outcome: GuardianOutcome;
|
|
@@ -64,7 +72,7 @@ export interface CircuitBreakerResult {
|
|
|
64
72
|
export declare function checkCircuitBreaker(state: GuardianState, limits: GuardianLimits): CircuitBreakerResult;
|
|
65
73
|
export declare function parseVerdict(raw: string): GuardianVerdict | null;
|
|
66
74
|
export declare function formatGuardianSubtotal(state: GuardianState, limits: GuardianLimits): string;
|
|
67
|
-
export type GuardianError = "timeout" | "malformed" | "network" | "empty";
|
|
75
|
+
export type GuardianError = "timeout" | "malformed" | "network" | "empty" | "aborted";
|
|
68
76
|
export interface ReviewResult {
|
|
69
77
|
verdict: GuardianVerdict | null;
|
|
70
78
|
error?: GuardianError;
|
|
@@ -76,6 +84,14 @@ export interface ReviewCommandDeps {
|
|
|
76
84
|
signal?: AbortSignal;
|
|
77
85
|
/** Override the model tier (default: efficient). */
|
|
78
86
|
modelTier?: string;
|
|
87
|
+
/** Consult timeout in ms (default: DEFAULT_GUARDIAN_LIMITS.timeoutMs). */
|
|
88
|
+
timeoutMs?: number;
|
|
89
|
+
/**
|
|
90
|
+
* The exec policy's justification for routing this command to the Guardian
|
|
91
|
+
* ("pushes to remote — confirm intent"). Included in the consult prompt so
|
|
92
|
+
* an `ask` rationale can add information beyond the static rule text.
|
|
93
|
+
*/
|
|
94
|
+
execJustification?: string;
|
|
79
95
|
}
|
|
80
96
|
/**
|
|
81
97
|
* The synthetic stage a Guardian consult runs as. Borrows the `plan` StageId
|
|
@@ -13,11 +13,19 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Trigger: the exec policy classifies a bash command as "prompt" (not clearly
|
|
15
15
|
* safe, not clearly forbidden). The Guardian reviews it instead of interrupting
|
|
16
|
-
* the user. On allow, the command runs. On
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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.
|
|
19
22
|
*
|
|
20
|
-
* Circuit breaker: 3 consecutive denials
|
|
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.
|
|
21
29
|
*/
|
|
22
30
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
23
31
|
export const DEFAULT_GUARDIAN_LIMITS = {
|
|
@@ -49,9 +57,12 @@ export function makeGuardianState() {
|
|
|
49
57
|
if (outcome === "deny") {
|
|
50
58
|
state.consecutiveDenials += 1;
|
|
51
59
|
}
|
|
52
|
-
else {
|
|
60
|
+
else if (outcome === "allow") {
|
|
53
61
|
state.consecutiveDenials = 0;
|
|
54
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).
|
|
55
66
|
return { ...state };
|
|
56
67
|
},
|
|
57
68
|
resetTurn() {
|
|
@@ -77,7 +88,7 @@ export function parseVerdict(raw) {
|
|
|
77
88
|
const jsonStr = jsonMatch ? jsonMatch[0] : raw;
|
|
78
89
|
const parsed = JSON.parse(jsonStr);
|
|
79
90
|
const outcome = parsed?.outcome;
|
|
80
|
-
if (outcome !== "allow" && outcome !== "deny")
|
|
91
|
+
if (outcome !== "allow" && outcome !== "ask" && outcome !== "deny")
|
|
81
92
|
return null;
|
|
82
93
|
const riskLevel = parsed.riskLevel;
|
|
83
94
|
const validLevels = ["low", "medium", "high", "critical"];
|
|
@@ -124,8 +135,9 @@ export function guardianStage(modelTier = GUARDIAN_MODEL_TIER) {
|
|
|
124
135
|
export async function reviewCommand(command, deps) {
|
|
125
136
|
const runStage = deps.runStage ?? defaultRunStage;
|
|
126
137
|
const stage = guardianStage(deps.modelTier);
|
|
127
|
-
// Build the abort signal with the Guardian timeout
|
|
128
|
-
|
|
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;
|
|
129
141
|
const controller = new AbortController();
|
|
130
142
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
131
143
|
timer.unref?.();
|
|
@@ -137,13 +149,23 @@ export async function reviewCommand(command, deps) {
|
|
|
137
149
|
deps.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
138
150
|
}
|
|
139
151
|
try {
|
|
140
|
-
const
|
|
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}` }, {
|
|
141
156
|
cwd: deps.cwd,
|
|
142
157
|
signal: controller.signal,
|
|
143
158
|
callerLabel: "guardian",
|
|
144
159
|
});
|
|
145
160
|
const cost = result.usage?.cost ?? 0;
|
|
146
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
|
+
}
|
|
147
169
|
if (!output) {
|
|
148
170
|
return { verdict: null, error: "empty", cost };
|
|
149
171
|
}
|
|
@@ -154,8 +176,13 @@ export async function reviewCommand(command, deps) {
|
|
|
154
176
|
return { verdict, cost };
|
|
155
177
|
}
|
|
156
178
|
catch (err) {
|
|
157
|
-
// Distinguish
|
|
158
|
-
|
|
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) {
|
|
159
186
|
return { verdict: null, error: "timeout", cost: 0 };
|
|
160
187
|
}
|
|
161
188
|
return { verdict: null, error: "network", cost: 0 };
|
package/dist/extension/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
4
5
|
import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
|
|
6
|
+
import { appendGrant, loadGrants, resolveRepoKey, storagePrefix } from "./approvedPrefixes.js";
|
|
7
|
+
import { redactCommand } from "./redact.js";
|
|
5
8
|
import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand } from "./guardian.js";
|
|
6
9
|
import { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
7
10
|
import { makeAskYagniTool } from "./askYagniTool.js";
|
|
@@ -127,7 +130,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
127
130
|
// after_provider_response event does NOT fire on a 401 (the OpenAI SDK throws
|
|
128
131
|
// before onResponse is reached), so message_end is the only seam.
|
|
129
132
|
let lastAuthRecovery = null;
|
|
130
|
-
const { models: fullCatalog, guardianEnabled: workspaceGuardianEnabled } = await fetchCatalog({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
133
|
+
const { models: fullCatalog, guardianEnabled: workspaceGuardianEnabled, guardianStorage: guardianStorageTier, } = await fetchCatalog({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
131
134
|
// Lock the interactive session to the `advanced` tier only. The backend
|
|
132
135
|
// catalog returns all tiers, but only `advanced` is registered with the
|
|
133
136
|
// `yagni` provider, so /model and Ctrl+P show a single entry. Child
|
|
@@ -245,6 +248,23 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
245
248
|
!workspaceGuardianEnabled;
|
|
246
249
|
const guardianTier = env.YAGNI_GUARDIAN_TIER ?? GUARDIAN_MODEL_TIER;
|
|
247
250
|
const guardianLimits = resolveGuardianLimits(env);
|
|
251
|
+
// YAG-510: guardian.log stays the sanitized local debug sink (hash-only,
|
|
252
|
+
// never the command). The remote guardian-events stream below is the
|
|
253
|
+
// separate, opt-in, per-workspace analytics sink; the two are independent.
|
|
254
|
+
const guardianLogSink = (payload) => {
|
|
255
|
+
try {
|
|
256
|
+
if (process.env.NODE_TEST_CONTEXT)
|
|
257
|
+
return;
|
|
258
|
+
const logPath = join(codeStateHome(null), "logs", "guardian.log");
|
|
259
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
260
|
+
appendFileSync(logPath, JSON.stringify({ ts: new Date().toISOString(), ...payload }) + "\n", "utf8");
|
|
261
|
+
}
|
|
262
|
+
catch { /* logging must never break the session */ }
|
|
263
|
+
};
|
|
264
|
+
// YAG-510: persisted "don't ask again" grants, per-repo keyed. Loaded once
|
|
265
|
+
// at startup (grants added by other concurrent sessions appear next launch).
|
|
266
|
+
const sessionGrants = evalMode ? [] : loadGrants();
|
|
267
|
+
const GUARDIAN_EVENT_TIMEOUT_MS = 5_000;
|
|
248
268
|
registerPermissionGate(pi, {
|
|
249
269
|
modeHolder,
|
|
250
270
|
guardianState,
|
|
@@ -252,16 +272,63 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
252
272
|
guardianTier,
|
|
253
273
|
guardianDisabled,
|
|
254
274
|
guardianReview: (command, deps) => reviewCommand(command, { ...deps, modelTier: guardianTier }),
|
|
255
|
-
onGuardianReview: (ev) =>
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
appendFileSync(logPath, JSON.stringify({ ts: new Date().toISOString(), ...ev }) + "\n", "utf8");
|
|
262
|
-
}
|
|
263
|
-
catch { /* logging must never break the session */ }
|
|
275
|
+
onGuardianReview: (ev) => guardianLogSink(ev),
|
|
276
|
+
grants: sessionGrants,
|
|
277
|
+
resolveRepoKey,
|
|
278
|
+
persistGrant: (grant) => {
|
|
279
|
+
if (!evalMode)
|
|
280
|
+
appendGrant(grant);
|
|
264
281
|
},
|
|
282
|
+
// Opt-in storage stream (YAG-510). Tier decides what leaves the machine:
|
|
283
|
+
// "off" → nothing (not even sent); "hash" → sha256 + family prefix +
|
|
284
|
+
// metadata, no command content; "raw" → adds client-REDACTED command and
|
|
285
|
+
// rationale. Fire-and-forget: one attempt, short timeout, failures logged
|
|
286
|
+
// fail-soft to guardian.log — a storage outage never touches the session.
|
|
287
|
+
onGuardianEvent: guardianStorageTier === "off" || evalMode
|
|
288
|
+
? undefined
|
|
289
|
+
: (ev) => {
|
|
290
|
+
void (async () => {
|
|
291
|
+
try {
|
|
292
|
+
const body = {
|
|
293
|
+
sessionId: env.YAGNI_SESSION_ID ?? null,
|
|
294
|
+
commandHash: createHash("sha256").update(ev.command).digest("hex"),
|
|
295
|
+
commandPrefix: storagePrefix(ev.command),
|
|
296
|
+
outcome: ev.outcome,
|
|
297
|
+
mode: ev.mode,
|
|
298
|
+
...(ev.execJustification ? { execJustification: ev.execJustification } : {}),
|
|
299
|
+
...(ev.riskLevel ? { riskLevel: ev.riskLevel } : {}),
|
|
300
|
+
...(ev.tier ? { tier: ev.tier } : {}),
|
|
301
|
+
...(ev.durationMs !== undefined ? { durationMs: ev.durationMs } : {}),
|
|
302
|
+
};
|
|
303
|
+
if (guardianStorageTier === "raw") {
|
|
304
|
+
body.command = redactCommand(ev.command);
|
|
305
|
+
if (ev.rationale)
|
|
306
|
+
body.rationale = redactCommand(ev.rationale);
|
|
307
|
+
}
|
|
308
|
+
const res = await resilientFetch(`${baseUrl}/api/yagni-code/guardian-events`, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: {
|
|
311
|
+
"content-type": "application/json",
|
|
312
|
+
authorization: `Bearer ${getTokenFn() ?? ""}`,
|
|
313
|
+
...attributionHeaders(deps.env),
|
|
314
|
+
},
|
|
315
|
+
body: JSON.stringify(body),
|
|
316
|
+
}, {
|
|
317
|
+
fetchImpl: authedFetch,
|
|
318
|
+
policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
|
|
319
|
+
});
|
|
320
|
+
if (!res.ok) {
|
|
321
|
+
guardianLogSink({ event: "guardian_event_post_failed", status: res.status });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
guardianLogSink({
|
|
326
|
+
event: "guardian_event_post_failed",
|
|
327
|
+
error: err instanceof Error ? err.message : "unknown",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
})();
|
|
331
|
+
},
|
|
265
332
|
...(mcpMutatingTools.length > 0
|
|
266
333
|
? {
|
|
267
334
|
policy: {
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
* the context so the model doesn't keep believing it is restricted.
|
|
28
28
|
*/
|
|
29
29
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import { type ApprovedPrefixGrant } from "./approvedPrefixes.js";
|
|
30
31
|
import { type BlessStore } from "./bless.js";
|
|
31
32
|
import { type ExecPolicy } from "./execPolicy.js";
|
|
33
|
+
import { type GuardianError, type GuardianRiskLevel } from "./guardian.js";
|
|
32
34
|
export type PermissionMode = "auto" | "plan" | "review";
|
|
33
35
|
/**
|
|
34
36
|
* A shared, mutable holder for the current permission mode. Both
|
|
@@ -76,6 +78,12 @@ export interface GateDecision {
|
|
|
76
78
|
* command string is available.
|
|
77
79
|
*/
|
|
78
80
|
classify?: "allow" | "prompt" | "forbidden";
|
|
81
|
+
/**
|
|
82
|
+
* The exec policy's justification for a "prompt" classification — why the
|
|
83
|
+
* command was routed to the Guardian. Threaded into the consult prompt and
|
|
84
|
+
* the storage event (it is the layer-tuning signal, YAG-510).
|
|
85
|
+
*/
|
|
86
|
+
classifyJustification?: string;
|
|
79
87
|
}
|
|
80
88
|
/**
|
|
81
89
|
* Pure permission decision for one tool call under a mode + policy. Auto allows
|
|
@@ -83,6 +91,35 @@ export interface GateDecision {
|
|
|
83
91
|
* unless a recorded decision blesses them.
|
|
84
92
|
*/
|
|
85
93
|
export declare function decideGate(toolName: string, params: Record<string, unknown>, mode: PermissionMode, policy: PermissionPolicy): GateDecision;
|
|
94
|
+
/**
|
|
95
|
+
* Terminal outcome of one prompt-band decision (YAG-510). One storage event
|
|
96
|
+
* is emitted per terminal outcome; `consulted` says whether a Guardian LLM
|
|
97
|
+
* call actually happened (grants/cache hits skip it).
|
|
98
|
+
*/
|
|
99
|
+
export type GuardianGateOutcome = "prefix_allow" | "cached_allow" | "allow" | "deny" | "ask_approved" | "ask_approved_remembered" | "ask_denied" | "ask_headless_blocked" | "breaker_ask_approved" | "breaker_blocked" | GuardianError;
|
|
100
|
+
/**
|
|
101
|
+
* Rich per-decision event for opt-in storage (YAG-510). Carries the RAW
|
|
102
|
+
* command — the wiring layer (index.ts) hashes/redacts per the workspace's
|
|
103
|
+
* storage tier before anything leaves the machine. Distinct from the
|
|
104
|
+
* sanitized GuardianDiagnosticEvent (local debug log), which never carries
|
|
105
|
+
* the command.
|
|
106
|
+
*/
|
|
107
|
+
export interface GuardianGateEvent {
|
|
108
|
+
command: string;
|
|
109
|
+
outcome: GuardianGateOutcome;
|
|
110
|
+
/** Why the exec policy routed this command to the Guardian. */
|
|
111
|
+
execJustification?: string;
|
|
112
|
+
riskLevel?: GuardianRiskLevel;
|
|
113
|
+
rationale?: string;
|
|
114
|
+
mode: PermissionMode;
|
|
115
|
+
tier?: string;
|
|
116
|
+
durationMs?: number;
|
|
117
|
+
/** Whether a Guardian LLM consult actually ran for this decision. */
|
|
118
|
+
consulted: boolean;
|
|
119
|
+
/** Present when the terminal outcome followed a Guardian error (e.g. an
|
|
120
|
+
* error-fallback ask that the user then approved). */
|
|
121
|
+
guardianError?: GuardianError;
|
|
122
|
+
}
|
|
86
123
|
/** What was blessed with "don't ask again", handed to the capture hook. */
|
|
87
124
|
export interface BlessRememberInfo {
|
|
88
125
|
tool: string;
|
|
@@ -123,6 +160,24 @@ export interface RegisterPermissionDeps {
|
|
|
123
160
|
onGuardianReview?: (event: import("./guardian.js").GuardianDiagnosticEvent) => void;
|
|
124
161
|
/** Disable the Guardian entirely (env var YAGNI_DISABLE_GUARDIAN). */
|
|
125
162
|
guardianDisabled?: boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Persisted "don't ask again" grants for this session's repo (YAG-510).
|
|
165
|
+
* Consulted ONLY after the exec policy classified a command as "prompt" —
|
|
166
|
+
* a grant can never override the forbidden band. Suppressed in review mode.
|
|
167
|
+
*/
|
|
168
|
+
grants?: readonly ApprovedPrefixGrant[];
|
|
169
|
+
/** Resolve the grant scope key for a cwd (git remote origin URL fallback
|
|
170
|
+
* realpath). Lazily invoked on the first prompt-band bash call. */
|
|
171
|
+
resolveRepoKey?: (cwd: string) => string;
|
|
172
|
+
/** Persist a new grant (fire-and-forget; the in-memory list is updated
|
|
173
|
+
* either way). index.ts wires approvedPrefixes.appendGrant. */
|
|
174
|
+
persistGrant?: (grant: ApprovedPrefixGrant) => void;
|
|
175
|
+
/**
|
|
176
|
+
* Called (fire-and-forget) at every terminal prompt-band outcome with the
|
|
177
|
+
* rich storage event (raw command — the wiring layer redacts/hashes).
|
|
178
|
+
* Fail-soft; never blocks.
|
|
179
|
+
*/
|
|
180
|
+
onGuardianEvent?: (event: GuardianGateEvent) => void;
|
|
126
181
|
}
|
|
127
182
|
/** The customType tag on injected mode-context messages (filterable later). */
|
|
128
183
|
export declare const MODE_CONTEXT_TYPE = "yagni-mode-context";
|