@yagni-app/code 0.2.1 → 0.3.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/cli.d.ts +30 -0
- package/dist/cli.js +147 -3
- package/dist/connectClaudeCode.d.ts +77 -0
- package/dist/connectClaudeCode.js +228 -0
- package/dist/connectCodex.d.ts +75 -0
- package/dist/connectCodex.js +201 -0
- 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 +103 -0
- package/dist/extension/approvedPrefixes.js +282 -0
- package/dist/extension/askAdvisorTool.d.ts +20 -5
- package/dist/extension/askAdvisorTool.js +126 -20
- package/dist/extension/askYagniTool.d.ts +23 -0
- package/dist/extension/askYagniTool.js +91 -2
- package/dist/extension/branding.d.ts +34 -3
- package/dist/extension/branding.js +114 -13
- 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 +44 -1
- package/dist/extension/config.js +37 -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 +135 -0
- package/dist/extension/execPolicy.js +936 -0
- package/dist/extension/flywheel.d.ts +44 -0
- package/dist/extension/flywheel.js +53 -0
- package/dist/extension/footer.d.ts +118 -0
- package/dist/extension/footer.js +308 -0
- package/dist/extension/guardian.d.ts +139 -0
- package/dist/extension/guardian.js +237 -0
- package/dist/extension/index.d.ts +35 -7
- package/dist/extension/index.js +338 -33
- package/dist/extension/mineBeat.d.ts +95 -0
- package/dist/extension/mineBeat.js +193 -0
- package/dist/extension/permission.d.ts +124 -10
- package/dist/extension/permission.js +603 -52
- 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 +40 -9
- package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
- package/dist/extension/pipeline/goStatusCommands.js +61 -1
- package/dist/extension/pipeline/personas.js +26 -1
- package/dist/extension/pipeline/resilience.d.ts +2 -1
- package/dist/extension/pipeline/resilience.js +21 -2
- package/dist/extension/pipeline/runRegistry.d.ts +23 -1
- package/dist/extension/pipeline/runRegistry.js +57 -1
- 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/recordDecisionTool.d.ts +8 -0
- package/dist/extension/recordDecisionTool.js +24 -0
- package/dist/extension/redact.d.ts +20 -0
- package/dist/extension/redact.js +64 -0
- package/dist/extension/rerouteNotice.d.ts +3 -4
- package/dist/extension/rerouteNotice.js +20 -11
- package/dist/extension/subagentRender.d.ts +129 -0
- package/dist/extension/subagentRender.js +441 -0
- package/dist/extension/subagents.d.ts +11 -8
- package/dist/extension/subagents.js +162 -37
- package/dist/extension/ticketTools.d.ts +37 -0
- package/dist/extension/ticketTools.js +117 -0
- package/dist/extension/todos.d.ts +28 -1
- package/dist/extension/todos.js +76 -1
- package/dist/extension/tokenProvider.js +46 -5
- package/dist/extension/ultra.d.ts +27 -0
- package/dist/extension/ultra.js +76 -0
- package/dist/launch.d.ts +7 -0
- package/dist/launch.js +24 -12
- package/dist/login.d.ts +4 -2
- package/dist/login.js +19 -4
- 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/dist/token.d.ts +25 -0
- package/dist/token.js +45 -0
- package/package.json +6 -5
- package/dist/extension/boostCommand.d.ts +0 -144
- package/dist/extension/boostCommand.js +0 -263
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The opt-in repo seeding beat (org plane, Run 7).
|
|
3
|
+
*
|
|
4
|
+
* On the first interactive session in a repo whose repo-scoped decision
|
|
5
|
+
* ledger is empty (`/context?repo=` reports `repoDecisionCount: 0`), YAGNI
|
|
6
|
+
* Code offers — once — to read the repo's own decision record and seed the
|
|
7
|
+
* ledger: "Read this repo's ADRs and docs and seed the decision ledger?".
|
|
8
|
+
* One keystroke, then the backend extracts, dedupes, and banks at most 12
|
|
9
|
+
* `repo_mined` asserted rows, and the beat prints the banked count plus the
|
|
10
|
+
* Library review deep link.
|
|
11
|
+
*
|
|
12
|
+
* Never automatic, never a nag: a decline writes a per-repo marker so the
|
|
13
|
+
* beat does not recur; an accepted pass marks the repo only after the server
|
|
14
|
+
* confirms (a transport failure leaves the offer available next session).
|
|
15
|
+
* The corpus is collected at git HEAD (same echo-chamber guard as
|
|
16
|
+
* repoDocs.ts) — ADR directories, agent-instruction root docs, top-level
|
|
17
|
+
* docs/*.md, and recent merge subjects — bounded client-side to the same
|
|
18
|
+
* limits the backend enforces.
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import * as fs from "node:fs";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
import { METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
24
|
+
/**
|
|
25
|
+
* Client-side bounds on the mining corpus — mirror of the backend's
|
|
26
|
+
* MINE_INPUT_LIMITS so nothing is silently truncated server-side.
|
|
27
|
+
*/
|
|
28
|
+
export const MINE_BEAT_LIMITS = {
|
|
29
|
+
maxDocs: 40,
|
|
30
|
+
maxPathChars: 300,
|
|
31
|
+
maxExcerptChars: 8_000,
|
|
32
|
+
maxCommitSubjects: 200,
|
|
33
|
+
maxSubjectChars: 200,
|
|
34
|
+
};
|
|
35
|
+
const GIT_OPTS = {
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
38
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
39
|
+
};
|
|
40
|
+
function git(cwd, args) {
|
|
41
|
+
try {
|
|
42
|
+
return execFileSync("git", args, { cwd, ...GIT_OPTS });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Parse `owner/name` from an origin remote URL (ssh or https). */
|
|
49
|
+
export function parseRepoFullName(remote) {
|
|
50
|
+
if (!remote)
|
|
51
|
+
return undefined;
|
|
52
|
+
const m = remote.trim().match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/);
|
|
53
|
+
return m ? m[1] : undefined;
|
|
54
|
+
}
|
|
55
|
+
export const defaultMineBeatGit = {
|
|
56
|
+
repoFullName: (cwd) => parseRepoFullName(git(cwd, ["config", "--get", "remote.origin.url"])),
|
|
57
|
+
listAtHead: (cwd) => {
|
|
58
|
+
const out = git(cwd, ["ls-tree", "-r", "--name-only", "HEAD"]);
|
|
59
|
+
return out === null ? null : out.split("\n").filter(Boolean);
|
|
60
|
+
},
|
|
61
|
+
readAtHead: (cwd, rel) => git(cwd, ["show", `HEAD:${rel}`]),
|
|
62
|
+
mergeSubjects: (cwd, max) => {
|
|
63
|
+
const merges = git(cwd, ["log", "--merges", `-n`, String(max), "--pretty=%s"]);
|
|
64
|
+
const chosen = merges && merges.trim().length > 0
|
|
65
|
+
? merges
|
|
66
|
+
: // Squash-merge repos have no merge commits; plain subjects still
|
|
67
|
+
// carry the shipped-work trail. No further heuristics (spec flag).
|
|
68
|
+
git(cwd, ["log", `-n`, String(max), "--pretty=%s"]);
|
|
69
|
+
return (chosen ?? "").split("\n").map((s) => s.trim()).filter(Boolean).slice(0, max);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
/** Is this HEAD-tracked path part of the repo's decision record? */
|
|
73
|
+
export function isMineCandidate(rel) {
|
|
74
|
+
if (/^(?:docs\/)?adrs?\/[^/]+\.md$/i.test(rel))
|
|
75
|
+
return true;
|
|
76
|
+
if (/^(?:AGENTS|CLAUDE|CONTRIBUTING|CONTEXT|ARCHITECTURE)\.md$/i.test(rel))
|
|
77
|
+
return true;
|
|
78
|
+
if (/^docs\/[^/]+\.md$/i.test(rel))
|
|
79
|
+
return true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
/** Collect the HEAD-era mining corpus, bounded to MINE_BEAT_LIMITS. */
|
|
83
|
+
export function collectMineCorpus(cwd, gitImpl = defaultMineBeatGit) {
|
|
84
|
+
const tracked = gitImpl.listAtHead(cwd);
|
|
85
|
+
if (tracked === null)
|
|
86
|
+
return { docs: [], commitSubjects: [] };
|
|
87
|
+
const docs = [];
|
|
88
|
+
for (const rel of tracked) {
|
|
89
|
+
if (docs.length >= MINE_BEAT_LIMITS.maxDocs)
|
|
90
|
+
break;
|
|
91
|
+
if (rel.length > MINE_BEAT_LIMITS.maxPathChars)
|
|
92
|
+
continue;
|
|
93
|
+
if (!isMineCandidate(rel))
|
|
94
|
+
continue;
|
|
95
|
+
const body = gitImpl.readAtHead(cwd, rel);
|
|
96
|
+
if (body === null || body.trim().length === 0)
|
|
97
|
+
continue;
|
|
98
|
+
docs.push({ path: rel, excerpt: body.trim().slice(0, MINE_BEAT_LIMITS.maxExcerptChars) });
|
|
99
|
+
}
|
|
100
|
+
const commitSubjects = gitImpl
|
|
101
|
+
.mergeSubjects(cwd, MINE_BEAT_LIMITS.maxCommitSubjects)
|
|
102
|
+
.map((s) => s.slice(0, MINE_BEAT_LIMITS.maxSubjectChars));
|
|
103
|
+
return { docs, commitSubjects };
|
|
104
|
+
}
|
|
105
|
+
/** Filesystem path of one repo's beat marker under the state home. */
|
|
106
|
+
export function mineMarkerPath(stateHome, repoFullName) {
|
|
107
|
+
return join(stateHome, "mine-beat", `${repoFullName.replace(/[/\\]/g, "__")}.json`);
|
|
108
|
+
}
|
|
109
|
+
export function fileMineBeatMarkers(stateHome) {
|
|
110
|
+
return {
|
|
111
|
+
has: (repo) => {
|
|
112
|
+
try {
|
|
113
|
+
return fs.existsSync(mineMarkerPath(stateHome, repo));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
write: (repo, status) => {
|
|
120
|
+
try {
|
|
121
|
+
const p = mineMarkerPath(stateHome, repo);
|
|
122
|
+
fs.mkdirSync(dirname(p), { recursive: true });
|
|
123
|
+
fs.writeFileSync(p, JSON.stringify({ status, at: new Date().toISOString() }), "utf8");
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// A marker failure must never break the session; worst case the beat
|
|
127
|
+
// asks once more next launch.
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Offer (once per repo) and, on yes, run the seeding pass. Fail-soft
|
|
134
|
+
* throughout: any error surfaces as a notify at most, never a broken session.
|
|
135
|
+
*/
|
|
136
|
+
export async function maybeOfferMiningBeat(ctx, opts) {
|
|
137
|
+
const { repoFullName } = opts;
|
|
138
|
+
// Only an EXPLICIT zero fires the beat: an absent count means an older
|
|
139
|
+
// backend or a count failure, and a nag on either would be wrong.
|
|
140
|
+
if (!repoFullName || opts.repoDecisionCount !== 0)
|
|
141
|
+
return { offered: false };
|
|
142
|
+
if (opts.markers.has(repoFullName))
|
|
143
|
+
return { offered: false };
|
|
144
|
+
if (!ctx.hasUI || typeof ctx.ui.confirm !== "function")
|
|
145
|
+
return { offered: false };
|
|
146
|
+
const collect = opts.collect ?? collectMineCorpus;
|
|
147
|
+
const corpus = collect(opts.cwd, opts.gitImpl ?? defaultMineBeatGit);
|
|
148
|
+
// Nothing to mine yet: skip WITHOUT a marker, so a later checkout that
|
|
149
|
+
// gains docs still gets its one offer.
|
|
150
|
+
if (corpus.docs.length === 0 && corpus.commitSubjects.length === 0) {
|
|
151
|
+
return { offered: false };
|
|
152
|
+
}
|
|
153
|
+
const accepted = await ctx.ui.confirm("Seed the decision ledger?", `Read this repo's ADRs and docs and bank up to 12 candidate decisions for ${repoFullName}? You review every one in the Library before it counts as confirmed.`);
|
|
154
|
+
if (!accepted) {
|
|
155
|
+
opts.markers.write(repoFullName, "declined");
|
|
156
|
+
return { offered: true, accepted: false };
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/decisions/mine`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
163
|
+
"content-type": "application/json",
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
repo: repoFullName,
|
|
167
|
+
docs: corpus.docs,
|
|
168
|
+
commitSubjects: corpus.commitSubjects,
|
|
169
|
+
// Stable per repo: the write-spool replay contract makes a retry of
|
|
170
|
+
// the same pass idempotent server-side.
|
|
171
|
+
idempotencyKey: `mine:${repoFullName}`,
|
|
172
|
+
}),
|
|
173
|
+
}, { fetchImpl: opts.fetchImpl, policy: METERED_POST_FETCH_POLICY });
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
// No marker: the offer stays available next session.
|
|
176
|
+
ctx.ui.notify("Seeding the decision ledger failed; YAGNI Code will offer again next session.", "error");
|
|
177
|
+
return { offered: true, accepted: true };
|
|
178
|
+
}
|
|
179
|
+
const body = (await res.json());
|
|
180
|
+
opts.markers.write(repoFullName, "seeded");
|
|
181
|
+
if (body.alreadyMined)
|
|
182
|
+
return { offered: true, accepted: true, banked: 0 };
|
|
183
|
+
const banked = typeof body.banked === "number" ? body.banked : 0;
|
|
184
|
+
const noun = banked === 1 ? "decision" : "decisions";
|
|
185
|
+
ctx.ui.notify(`Banked ${banked} ${noun} read from this repo's docs. Review them: ${opts.baseUrl}/knowledge/decisions`, "info");
|
|
186
|
+
return { offered: true, accepted: true, banked };
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
ctx.ui.notify("Seeding the decision ledger failed; YAGNI Code will offer again next session.", "error");
|
|
190
|
+
return { offered: true, accepted: true };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=mineBeat.js.map
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* YAGNI Code registers no tool_call handler today, so the interactive session has
|
|
5
5
|
* no plan/approval surface. P3 adds one on pi's documented `tool_call` block seam
|
|
6
6
|
* plus a `/mode` command:
|
|
7
|
-
* - auto (default):
|
|
7
|
+
* - auto (default): ordinary coding tools run directly; external tracker
|
|
8
|
+
* changes still require fresh human confirmation.
|
|
8
9
|
* - plan : blocks write/edit/bash so the agent can explore + propose without
|
|
9
10
|
* touching the tree.
|
|
10
11
|
* - review : surfaces a three-way ctx.ui.select before a write/edit/bash; a
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
* adds a session-scoped bless rule AND drafts a decision capture.
|
|
15
16
|
*
|
|
16
17
|
* `decideGate` is PURE; the live wiring holds the mode in a small closure (no
|
|
17
|
-
* module-global state). The default auto mode remains
|
|
18
|
+
* module-global state). The default auto mode remains direct for coding tools, but stricter
|
|
18
19
|
* modes fail closed for side-effect tools if the gate itself errors. Bless rules
|
|
19
20
|
* are session-scoped, path-prefix-bound, never persisted, and never consulted in
|
|
20
21
|
* plan mode (plan blocks outright before isBlessed is reached).
|
|
@@ -26,20 +27,43 @@
|
|
|
26
27
|
* the context so the model doesn't keep believing it is restricted.
|
|
27
28
|
*/
|
|
28
29
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import { type ApprovedPrefixGrant } from "./approvedPrefixes.js";
|
|
29
31
|
import { type BlessStore } from "./bless.js";
|
|
32
|
+
import { type ExecPolicy } from "./execPolicy.js";
|
|
33
|
+
import { type GuardianError, type GuardianRiskLevel } from "./guardian.js";
|
|
30
34
|
export type PermissionMode = "auto" | "plan" | "review";
|
|
35
|
+
/**
|
|
36
|
+
* A shared, mutable holder for the current permission mode. Both
|
|
37
|
+
* `registerPermissionGate` (which writes on /mode) and the footer factory
|
|
38
|
+
* (which reads on every render) hold a reference, so mode changes appear
|
|
39
|
+
* immediately in the footer's line 2.
|
|
40
|
+
*/
|
|
41
|
+
export interface ModeHolder {
|
|
42
|
+
get(): PermissionMode;
|
|
43
|
+
set(m: PermissionMode): void;
|
|
44
|
+
onSet(fn: (m: PermissionMode) => void): void;
|
|
45
|
+
}
|
|
46
|
+
export declare function createModeHolder(initial?: PermissionMode): ModeHolder;
|
|
31
47
|
/** Which tools each tier acts on, plus the optional grounding-bless predicate. */
|
|
32
48
|
export interface PermissionPolicy {
|
|
33
49
|
/** Tools blocked outright in plan mode (write/exec). */
|
|
34
50
|
planBlockTools: string[];
|
|
35
51
|
/** Tools that prompt for confirmation in review mode. */
|
|
36
52
|
reviewConfirmTools: string[];
|
|
53
|
+
/** Consequential external writes that require fresh consent in every mode. */
|
|
54
|
+
alwaysConfirmTools?: string[];
|
|
37
55
|
/**
|
|
38
56
|
* Optional: a recorded decision already blesses this action, so it auto-runs in
|
|
39
57
|
* review mode instead of prompting. The hook for tying the gate to captured
|
|
40
58
|
* judgment; default undefined (nothing pre-blessed).
|
|
41
59
|
*/
|
|
42
60
|
isBlessed?: (toolName: string, params: Record<string, unknown>) => boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Exec policy for bash command classification. When present, bash commands are
|
|
63
|
+
* classified by the exec policy engine before the tool-granular logic runs.
|
|
64
|
+
* Defaults to DEFAULT_EXEC_POLICY when absent.
|
|
65
|
+
*/
|
|
66
|
+
execPolicy?: ExecPolicy;
|
|
43
67
|
}
|
|
44
68
|
export declare const DEFAULT_PERMISSION_POLICY: PermissionPolicy;
|
|
45
69
|
/** A pure gate verdict: block outright, ask to confirm, or allow. */
|
|
@@ -48,13 +72,55 @@ export interface GateDecision {
|
|
|
48
72
|
reason?: string;
|
|
49
73
|
/** review mode only: the caller should ctx.ui.confirm before allowing. */
|
|
50
74
|
confirm?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Exec policy classification result for bash commands. When present, the
|
|
77
|
+
* tool_call handler should run the Guardian for "prompt" before falling
|
|
78
|
+
* through to the user confirm. Absent for non-bash tools or when no
|
|
79
|
+
* command string is available.
|
|
80
|
+
*/
|
|
81
|
+
classify?: "allow" | "prompt" | "forbidden";
|
|
82
|
+
/**
|
|
83
|
+
* The exec policy's justification for a "prompt" classification — why the
|
|
84
|
+
* command was routed to the Guardian. Threaded into the consult prompt and
|
|
85
|
+
* the storage event (it is the layer-tuning signal, YAG-510).
|
|
86
|
+
*/
|
|
87
|
+
classifyJustification?: string;
|
|
51
88
|
}
|
|
52
89
|
/**
|
|
53
|
-
* Pure permission decision for one tool call under a mode + policy.
|
|
54
|
-
*
|
|
90
|
+
* Pure permission decision for one tool call under a mode + policy. Auto allows
|
|
91
|
+
* ordinary tools; plan blocks the write/exec set; review marks writes for confirmation
|
|
55
92
|
* unless a recorded decision blesses them.
|
|
56
93
|
*/
|
|
57
94
|
export declare function decideGate(toolName: string, params: Record<string, unknown>, mode: PermissionMode, policy: PermissionPolicy): GateDecision;
|
|
95
|
+
/**
|
|
96
|
+
* Terminal outcome of one prompt-band decision (YAG-510). One storage event
|
|
97
|
+
* is emitted per terminal outcome; `consulted` says whether a Guardian LLM
|
|
98
|
+
* call actually happened (grants/cache hits skip it).
|
|
99
|
+
*/
|
|
100
|
+
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;
|
|
101
|
+
/**
|
|
102
|
+
* Rich per-decision event for opt-in storage (YAG-510). Carries the RAW
|
|
103
|
+
* command — the wiring layer (index.ts) hashes/redacts per the workspace's
|
|
104
|
+
* storage tier before anything leaves the machine. Distinct from the
|
|
105
|
+
* sanitized GuardianDiagnosticEvent (local debug log), which never carries
|
|
106
|
+
* the command.
|
|
107
|
+
*/
|
|
108
|
+
export interface GuardianGateEvent {
|
|
109
|
+
command: string;
|
|
110
|
+
outcome: GuardianGateOutcome;
|
|
111
|
+
/** Why the exec policy routed this command to the Guardian. */
|
|
112
|
+
execJustification?: string;
|
|
113
|
+
riskLevel?: GuardianRiskLevel;
|
|
114
|
+
rationale?: string;
|
|
115
|
+
mode: PermissionMode;
|
|
116
|
+
tier?: string;
|
|
117
|
+
durationMs?: number;
|
|
118
|
+
/** Whether a Guardian LLM consult actually ran for this decision. */
|
|
119
|
+
consulted: boolean;
|
|
120
|
+
/** Present when the terminal outcome followed a Guardian error (e.g. an
|
|
121
|
+
* error-fallback ask that the user then approved). */
|
|
122
|
+
guardianError?: GuardianError;
|
|
123
|
+
}
|
|
58
124
|
/** What was blessed with "don't ask again", handed to the capture hook. */
|
|
59
125
|
export interface BlessRememberInfo {
|
|
60
126
|
tool: string;
|
|
@@ -73,16 +139,64 @@ export interface RegisterPermissionDeps {
|
|
|
73
139
|
* never blocks the approved tool call.
|
|
74
140
|
*/
|
|
75
141
|
onBlessRemember?: (ctx: ExtensionContext, info: BlessRememberInfo) => void | Promise<void>;
|
|
142
|
+
/** Shared holder so the footer can read the live mode on every render. */
|
|
143
|
+
modeHolder?: ModeHolder;
|
|
144
|
+
/**
|
|
145
|
+
* Guardian state handle for the session. When present, prompt-band bash
|
|
146
|
+
* commands are auto-reviewed by the Guardian LLM instead of interrupting
|
|
147
|
+
* the user. When absent, prompt-band commands fall through to the existing
|
|
148
|
+
* tool-granular behavior (review: user prompt, auto: allow).
|
|
149
|
+
*/
|
|
150
|
+
guardianState?: import("./guardian.js").GuardianStateHandle;
|
|
151
|
+
/** Guardian limits (timeouts, circuit breaker). Defaults to DEFAULT_GUARDIAN_LIMITS. */
|
|
152
|
+
guardianLimits?: import("./guardian.js").GuardianLimits;
|
|
153
|
+
/** Override the Guardian model tier (default: efficient). */
|
|
154
|
+
guardianTier?: string;
|
|
155
|
+
/** Injectable Guardian review function (tests pass a stub). */
|
|
156
|
+
guardianReview?: (command: string, deps: import("./guardian.js").ReviewCommandDeps) => Promise<import("./guardian.js").ReviewResult>;
|
|
157
|
+
/**
|
|
158
|
+
* Called (fire-and-forget) after each Guardian review with a sanitized
|
|
159
|
+
* diagnostic event. Fail-soft; never blocks.
|
|
160
|
+
*/
|
|
161
|
+
onGuardianReview?: (event: import("./guardian.js").GuardianDiagnosticEvent) => void;
|
|
162
|
+
/** Disable the Guardian entirely (env var YAGNI_DISABLE_GUARDIAN). */
|
|
163
|
+
guardianDisabled?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Persisted "don't ask again" grants for this session's repo (YAG-510).
|
|
166
|
+
* Consulted ONLY after the exec policy classified a command as "prompt" —
|
|
167
|
+
* a grant can never override the forbidden band. Suppressed in review mode.
|
|
168
|
+
*/
|
|
169
|
+
grants?: readonly ApprovedPrefixGrant[];
|
|
170
|
+
/** Resolve the grant scope key for a cwd (git remote origin URL fallback
|
|
171
|
+
* realpath). Lazily invoked on the first prompt-band bash call. */
|
|
172
|
+
resolveRepoKey?: (cwd: string) => string;
|
|
173
|
+
/** Persist a new grant (fire-and-forget; the in-memory list is updated
|
|
174
|
+
* either way). index.ts wires approvedPrefixes.appendGrant. */
|
|
175
|
+
persistGrant?: (grant: ApprovedPrefixGrant) => void;
|
|
176
|
+
/**
|
|
177
|
+
* Called (fire-and-forget) at every terminal prompt-band outcome with the
|
|
178
|
+
* rich storage event (raw command — the wiring layer redacts/hashes).
|
|
179
|
+
* Fail-soft; never blocks.
|
|
180
|
+
*/
|
|
181
|
+
onGuardianEvent?: (event: GuardianGateEvent) => void;
|
|
76
182
|
}
|
|
77
|
-
/** The customType tag on injected
|
|
78
|
-
export declare const
|
|
183
|
+
/** The customType tag on injected mode-context messages (filterable later). */
|
|
184
|
+
export declare const MODE_CONTEXT_TYPE = "yagni-mode-context";
|
|
185
|
+
/** Legacy alias — the original plan-mode tag, kept for backward compat. */
|
|
186
|
+
export declare const PLAN_CONTEXT_TYPE = "yagni-mode-context";
|
|
79
187
|
export declare const PLAN_CONTEXT_MESSAGE = "[PLAN MODE ACTIVE]\nYou are in plan mode: explore and design, change nothing.\n- write, edit, and bash are held by the permission gate; do not attempt them.\n- Read, search, and ask_yagni freely to ground the plan in how this company works.\n- Produce a concrete numbered plan of the steps you would take, with the files involved.\n- End by asking the user to review the plan; they run /mode auto (or /mode review) to execute it.\n- Once executing, track the plan's steps with todo_write.";
|
|
188
|
+
/** Build the mode-awareness context message for the current permission mode. */
|
|
189
|
+
export declare function buildModeContextMessage(mode: PermissionMode): string;
|
|
80
190
|
/**
|
|
81
|
-
* Drop previously injected
|
|
82
|
-
* model
|
|
83
|
-
*
|
|
191
|
+
* Drop previously injected mode-context messages from a DIFFERENT mode so the
|
|
192
|
+
* model does not keep believing it is in a prior mode. Messages matching the
|
|
193
|
+
* current mode are kept (the fresh injection from before_agent_start should
|
|
194
|
+
* survive). Pure; returns the SAME array when nothing needs filtering so callers
|
|
195
|
+
* can cheaply detect a no-op.
|
|
84
196
|
*/
|
|
85
|
-
export declare function
|
|
197
|
+
export declare function filterStaleModeContext<T>(messages: T[], currentMode?: PermissionMode): T[];
|
|
198
|
+
/** Legacy alias — the original plan-mode filter name. */
|
|
199
|
+
export declare const filterStalePlanContext: typeof filterStaleModeContext;
|
|
86
200
|
/**
|
|
87
201
|
* Wire the tool_call gate + the /mode command onto a shared mode holder. Default
|
|
88
202
|
* auto, so absent any /mode this is a no-op over today's behavior.
|