@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,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session state for the miss-to-record flywheel (org plane, Run 7).
|
|
3
|
+
*
|
|
4
|
+
* When `ask_yagni` answers `no_position`, the server attaches a
|
|
5
|
+
* `recordSuggestion` telling the agent to bank the assumption it proceeds on.
|
|
6
|
+
* The extension owns two client-side guards the server cannot:
|
|
7
|
+
*
|
|
8
|
+
* - **The per-run cap.** At most {@link FLYWHEEL_RUN_CAP} suggestions are
|
|
9
|
+
* surfaced per session; past it the suggestion is suppressed so a busy
|
|
10
|
+
* session cannot flood the ledger with asserted rows.
|
|
11
|
+
* - **Dedupe attribution.** A `record_decision` call that answers the
|
|
12
|
+
* QUESTION a surfaced suggestion asked about is a mid-run agent with no
|
|
13
|
+
* human to adjudicate a near-duplicate, so it sends `dedupe: true` (the
|
|
14
|
+
* backend then returns the existing row instead of inserting). The
|
|
15
|
+
* attribution is correlated to the suggested question — an unrelated
|
|
16
|
+
* record_decision (a different judgment the agent banks mid-run, or a
|
|
17
|
+
* human `/decide`) never inherits the flag.
|
|
18
|
+
*/
|
|
19
|
+
/** Most flywheel suggestions surfaced per session. */
|
|
20
|
+
export declare const FLYWHEEL_RUN_CAP = 3;
|
|
21
|
+
export interface FlywheelState {
|
|
22
|
+
/** Suggestions surfaced so far this session. */
|
|
23
|
+
suggestionsShown: number;
|
|
24
|
+
/**
|
|
25
|
+
* The question of the most recent surfaced suggestion, until a matching
|
|
26
|
+
* record_decision consumes it. Only a record answering THIS question is
|
|
27
|
+
* flywheel-attributed (sends `dedupe: true`).
|
|
28
|
+
*/
|
|
29
|
+
pendingQuestion: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function makeFlywheelState(): FlywheelState;
|
|
32
|
+
/** May another suggestion be surfaced? */
|
|
33
|
+
export declare function canSurfaceSuggestion(state: FlywheelState): boolean;
|
|
34
|
+
/** Record that a suggestion (asking about `question`) reached the model. */
|
|
35
|
+
export declare function noteSuggestionSurfaced(state: FlywheelState, question: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Consume the flywheel attribution for a record_decision call. Returns true
|
|
38
|
+
* (and clears the pending question) only when the recorded question matches
|
|
39
|
+
* the surfaced suggestion's; a mismatch leaves the attribution pending — the
|
|
40
|
+
* agent may record other judgments before circling back, and a fail-safe
|
|
41
|
+
* mismatch simply means a normal insert (no dedupe), never a swallowed write.
|
|
42
|
+
*/
|
|
43
|
+
export declare function consumeFlywheelAttribution(state: FlywheelState, recordedQuestion: string): boolean;
|
|
44
|
+
//# sourceMappingURL=flywheel.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session state for the miss-to-record flywheel (org plane, Run 7).
|
|
3
|
+
*
|
|
4
|
+
* When `ask_yagni` answers `no_position`, the server attaches a
|
|
5
|
+
* `recordSuggestion` telling the agent to bank the assumption it proceeds on.
|
|
6
|
+
* The extension owns two client-side guards the server cannot:
|
|
7
|
+
*
|
|
8
|
+
* - **The per-run cap.** At most {@link FLYWHEEL_RUN_CAP} suggestions are
|
|
9
|
+
* surfaced per session; past it the suggestion is suppressed so a busy
|
|
10
|
+
* session cannot flood the ledger with asserted rows.
|
|
11
|
+
* - **Dedupe attribution.** A `record_decision` call that answers the
|
|
12
|
+
* QUESTION a surfaced suggestion asked about is a mid-run agent with no
|
|
13
|
+
* human to adjudicate a near-duplicate, so it sends `dedupe: true` (the
|
|
14
|
+
* backend then returns the existing row instead of inserting). The
|
|
15
|
+
* attribution is correlated to the suggested question — an unrelated
|
|
16
|
+
* record_decision (a different judgment the agent banks mid-run, or a
|
|
17
|
+
* human `/decide`) never inherits the flag.
|
|
18
|
+
*/
|
|
19
|
+
/** Most flywheel suggestions surfaced per session. */
|
|
20
|
+
export const FLYWHEEL_RUN_CAP = 3;
|
|
21
|
+
export function makeFlywheelState() {
|
|
22
|
+
return { suggestionsShown: 0, pendingQuestion: null };
|
|
23
|
+
}
|
|
24
|
+
/** May another suggestion be surfaced? */
|
|
25
|
+
export function canSurfaceSuggestion(state) {
|
|
26
|
+
return state.suggestionsShown < FLYWHEEL_RUN_CAP;
|
|
27
|
+
}
|
|
28
|
+
/** Loose textual identity: case- and whitespace-insensitive, terminal punctuation ignored. */
|
|
29
|
+
function normalizeQuestion(question) {
|
|
30
|
+
return question.toLowerCase().replace(/\s+/g, " ").replace(/[.?!\s]+$/g, "").trim();
|
|
31
|
+
}
|
|
32
|
+
/** Record that a suggestion (asking about `question`) reached the model. */
|
|
33
|
+
export function noteSuggestionSurfaced(state, question) {
|
|
34
|
+
state.suggestionsShown += 1;
|
|
35
|
+
state.pendingQuestion = question;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Consume the flywheel attribution for a record_decision call. Returns true
|
|
39
|
+
* (and clears the pending question) only when the recorded question matches
|
|
40
|
+
* the surfaced suggestion's; a mismatch leaves the attribution pending — the
|
|
41
|
+
* agent may record other judgments before circling back, and a fail-safe
|
|
42
|
+
* mismatch simply means a normal insert (no dedupe), never a swallowed write.
|
|
43
|
+
*/
|
|
44
|
+
export function consumeFlywheelAttribution(state, recordedQuestion) {
|
|
45
|
+
if (state.pendingQuestion === null)
|
|
46
|
+
return false;
|
|
47
|
+
if (normalizeQuestion(state.pendingQuestion) !== normalizeQuestion(recordedQuestion)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
state.pendingQuestion = null;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=flywheel.js.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom footer for the YAGNI CLI — replaces pi's built-in footer.
|
|
3
|
+
*
|
|
4
|
+
* Renders three lines:
|
|
5
|
+
* 1. folder · [worktree] · branch (git context; ~-path fallback off-repo)
|
|
6
|
+
* 2. model · ↑in ↓out $cost · ctx% (session stats; context % is an integer)
|
|
7
|
+
* 3. extension statuses (branding, todo counter, mode) joined by " · "
|
|
8
|
+
*
|
|
9
|
+
* --- How to customize the status bar (for future tickets) ---
|
|
10
|
+
*
|
|
11
|
+
* LIFECYCLE: `ctx.ui.setFooter()` is NOT available at extension factory time.
|
|
12
|
+
* The `pi` (ExtensionAPI) object passed to the factory has no UI methods —
|
|
13
|
+
* they live on `ctx.ui` (ExtensionUIContext), which is only bound after
|
|
14
|
+
* `_applyExtensionBindings` runs, immediately before the `session_start`
|
|
15
|
+
* event. So the footer must be set inside a `pi.on("session_start", ...)`
|
|
16
|
+
* handler, guarded by `ctx.mode === "tui"` (no footer in RPC/print mode).
|
|
17
|
+
* See index.ts's session_start handler for the wiring, and pi's
|
|
18
|
+
* `docs/extensions.md` § "Widgets, Status, and Footer" + the `custom-header.ts`
|
|
19
|
+
* example for the canonical pattern.
|
|
20
|
+
*
|
|
21
|
+
* FACTORY SIGNATURE: `setFooter((tui, theme, footerData) => Component)` where:
|
|
22
|
+
* - `tui` — the TUI instance (screen dimensions, focus)
|
|
23
|
+
* - `theme` — the current Theme (use `theme.fg("dim", text)` etc.)
|
|
24
|
+
* - `footerData` — ReadonlyFooterDataProvider: `getGitBranch()`,
|
|
25
|
+
* `getExtensionStatuses()` (statuses set via `ctx.ui.setStatus(key, text)`),
|
|
26
|
+
* `getAvailableProviderCount()`
|
|
27
|
+
* Model info, token stats, and context usage are NOT on `footerData` — they're
|
|
28
|
+
* on `ctx` (the ExtensionContext passed to the session_start handler):
|
|
29
|
+
* `ctx.model`, `ctx.sessionManager`, `ctx.getContextUsage()`. Thread them
|
|
30
|
+
* through a closure if the footer needs them (this module's
|
|
31
|
+
* `createYagniFooterFactory` does exactly that).
|
|
32
|
+
*
|
|
33
|
+
* COMPONENT CONTRACT: the returned object needs `render(width: number): string[]`
|
|
34
|
+
* (returns the lines to display, one string per row) and `invalidate()` (called
|
|
35
|
+
* when the component should re-render). Optionally `dispose()` for cleanup.
|
|
36
|
+
*
|
|
37
|
+
* REFERENCE: pi's built-in footer lives at
|
|
38
|
+
* `node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js`
|
|
39
|
+
* — useful reference for what data to replicate and how to format it.
|
|
40
|
+
*/
|
|
41
|
+
import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
42
|
+
import type { ModeHolder, PermissionMode } from "./permission.js";
|
|
43
|
+
export declare const BRANCH_MAX_WIDTH = 60;
|
|
44
|
+
export declare function cyclePermissionMode(current: PermissionMode): PermissionMode;
|
|
45
|
+
export declare function isShiftTab(data: string): boolean;
|
|
46
|
+
export declare const GIT_MUTATING_PATTERN: RegExp;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the status bar's left pad from the launcher's `YAGNI_PAD_X` env so it
|
|
49
|
+
* aligns with the editor input and the chat/output area on one shared column.
|
|
50
|
+
* The launcher seeds the same value as pi's `editorPaddingX`; this footer can't
|
|
51
|
+
* read pi settings, so the value crosses over env. Falls back to the output
|
|
52
|
+
* area's default (1) when unset/invalid. Clamped to pi's 0–3 editor range.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveFooterPadX(raw?: string | undefined): number;
|
|
55
|
+
/** Format token counts for compact footer display (mirrors pi's formatTokens). */
|
|
56
|
+
export declare function formatTokens(count: number): string;
|
|
57
|
+
/** Shorten cwd relative to home, like pi's built-in footer. */
|
|
58
|
+
export declare function formatCwd(cwd: string, home: string | undefined): string;
|
|
59
|
+
interface UsageTotals {
|
|
60
|
+
input: number;
|
|
61
|
+
output: number;
|
|
62
|
+
cacheRead: number;
|
|
63
|
+
cacheWrite: number;
|
|
64
|
+
cost: number;
|
|
65
|
+
}
|
|
66
|
+
/** Accumulate usage from all session entries (mirrors pi's built-in footer). */
|
|
67
|
+
export declare function collectUsage(sessionManager: ExtensionContext["sessionManager"]): UsageTotals;
|
|
68
|
+
export interface GitInfo {
|
|
69
|
+
/** Repo-root folder basename (the MAIN repo, stable across worktrees), or ~-path off-repo. */
|
|
70
|
+
folder: string;
|
|
71
|
+
inRepo: boolean;
|
|
72
|
+
/** Current branch, null off-repo. "detached" on detached HEAD. */
|
|
73
|
+
branch: string | null;
|
|
74
|
+
/** Linked-worktree label for [brackets], or null to hide. */
|
|
75
|
+
worktree: string | null;
|
|
76
|
+
}
|
|
77
|
+
/** Git probes `resolveWorktreeLabel` needs, injectable so the rules can be unit-tested. */
|
|
78
|
+
export interface WorktreeProbes {
|
|
79
|
+
isLinkedWorktree(repoRoot: string): boolean;
|
|
80
|
+
worktrees(repoRoot: string): string[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Show [worktree] only on a LINKED worktree (never the main checkout), when the repo has
|
|
84
|
+
* >1 worktree, AND the dir differs from the branch slug. The main checkout is the default
|
|
85
|
+
* context and earns no label; a [bracket] only disambiguates a secondary working tree.
|
|
86
|
+
*/
|
|
87
|
+
export declare function resolveWorktreeLabel(repoRoot: string, branch: string | null, probes?: WorktreeProbes): string | null;
|
|
88
|
+
/**
|
|
89
|
+
* Detect git info for a cwd, gracefully. Never throws: non-git folders, missing git
|
|
90
|
+
* binary, and corrupt repos all degrade to a safe partial/empty result.
|
|
91
|
+
*/
|
|
92
|
+
export declare function detectGitInfo(cwd: string, home: string | undefined): GitInfo;
|
|
93
|
+
/** Pure line-builder, exported for tests. All data injected; colors via theme. */
|
|
94
|
+
export declare function renderFooterLines(input: {
|
|
95
|
+
git: GitInfo;
|
|
96
|
+
model: string;
|
|
97
|
+
/** Current permission mode; shown on line 2 to the left of the model as "<mode> mode". */
|
|
98
|
+
mode?: PermissionMode | null;
|
|
99
|
+
usage: UsageTotals;
|
|
100
|
+
contextPercent: number | null;
|
|
101
|
+
statuses: string[];
|
|
102
|
+
}, theme: Pick<Theme, "fg">, width: number, padX?: number): string[];
|
|
103
|
+
/**
|
|
104
|
+
* Create a footer factory that captures the session `ctx` (for session data)
|
|
105
|
+
* and returns the component `setFooter` expects. Called from the
|
|
106
|
+
* `session_start` handler in index.ts.
|
|
107
|
+
*/
|
|
108
|
+
export interface FooterInvalidateHandle {
|
|
109
|
+
invalidateGit(): void;
|
|
110
|
+
requestRender(): void;
|
|
111
|
+
}
|
|
112
|
+
export declare function createYagniFooterFactory(ctx: ExtensionContext, modeHolder?: ModeHolder, invalidateHandle?: FooterInvalidateHandle): (_tui: unknown, theme: Theme, footerData: ReadonlyFooterDataProvider) => {
|
|
113
|
+
render(width: number): string[];
|
|
114
|
+
invalidate(): void;
|
|
115
|
+
dispose(): void;
|
|
116
|
+
};
|
|
117
|
+
export {};
|
|
118
|
+
//# sourceMappingURL=footer.d.ts.map
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom footer for the YAGNI CLI — replaces pi's built-in footer.
|
|
3
|
+
*
|
|
4
|
+
* Renders three lines:
|
|
5
|
+
* 1. folder · [worktree] · branch (git context; ~-path fallback off-repo)
|
|
6
|
+
* 2. model · ↑in ↓out $cost · ctx% (session stats; context % is an integer)
|
|
7
|
+
* 3. extension statuses (branding, todo counter, mode) joined by " · "
|
|
8
|
+
*
|
|
9
|
+
* --- How to customize the status bar (for future tickets) ---
|
|
10
|
+
*
|
|
11
|
+
* LIFECYCLE: `ctx.ui.setFooter()` is NOT available at extension factory time.
|
|
12
|
+
* The `pi` (ExtensionAPI) object passed to the factory has no UI methods —
|
|
13
|
+
* they live on `ctx.ui` (ExtensionUIContext), which is only bound after
|
|
14
|
+
* `_applyExtensionBindings` runs, immediately before the `session_start`
|
|
15
|
+
* event. So the footer must be set inside a `pi.on("session_start", ...)`
|
|
16
|
+
* handler, guarded by `ctx.mode === "tui"` (no footer in RPC/print mode).
|
|
17
|
+
* See index.ts's session_start handler for the wiring, and pi's
|
|
18
|
+
* `docs/extensions.md` § "Widgets, Status, and Footer" + the `custom-header.ts`
|
|
19
|
+
* example for the canonical pattern.
|
|
20
|
+
*
|
|
21
|
+
* FACTORY SIGNATURE: `setFooter((tui, theme, footerData) => Component)` where:
|
|
22
|
+
* - `tui` — the TUI instance (screen dimensions, focus)
|
|
23
|
+
* - `theme` — the current Theme (use `theme.fg("dim", text)` etc.)
|
|
24
|
+
* - `footerData` — ReadonlyFooterDataProvider: `getGitBranch()`,
|
|
25
|
+
* `getExtensionStatuses()` (statuses set via `ctx.ui.setStatus(key, text)`),
|
|
26
|
+
* `getAvailableProviderCount()`
|
|
27
|
+
* Model info, token stats, and context usage are NOT on `footerData` — they're
|
|
28
|
+
* on `ctx` (the ExtensionContext passed to the session_start handler):
|
|
29
|
+
* `ctx.model`, `ctx.sessionManager`, `ctx.getContextUsage()`. Thread them
|
|
30
|
+
* through a closure if the footer needs them (this module's
|
|
31
|
+
* `createYagniFooterFactory` does exactly that).
|
|
32
|
+
*
|
|
33
|
+
* COMPONENT CONTRACT: the returned object needs `render(width: number): string[]`
|
|
34
|
+
* (returns the lines to display, one string per row) and `invalidate()` (called
|
|
35
|
+
* when the component should re-render). Optionally `dispose()` for cleanup.
|
|
36
|
+
*
|
|
37
|
+
* REFERENCE: pi's built-in footer lives at
|
|
38
|
+
* `node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js`
|
|
39
|
+
* — useful reference for what data to replicate and how to format it.
|
|
40
|
+
*/
|
|
41
|
+
import { spawnSync } from "node:child_process";
|
|
42
|
+
import { statSync } from "node:fs";
|
|
43
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
44
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
45
|
+
export const BRANCH_MAX_WIDTH = 60;
|
|
46
|
+
const WORKTREE_MAX_WIDTH = 30;
|
|
47
|
+
/** Section separator: single space + middle dot + single space. */
|
|
48
|
+
const SEP = " · ";
|
|
49
|
+
const MODE_CYCLE = ["auto", "review", "plan"];
|
|
50
|
+
export function cyclePermissionMode(current) {
|
|
51
|
+
const idx = MODE_CYCLE.indexOf(current);
|
|
52
|
+
return MODE_CYCLE[(idx + 1) % MODE_CYCLE.length];
|
|
53
|
+
}
|
|
54
|
+
export function isShiftTab(data) {
|
|
55
|
+
return matchesKey(data, "shift+tab");
|
|
56
|
+
}
|
|
57
|
+
export const GIT_MUTATING_PATTERN = /\bgit\s+(?:checkout|switch|branch|worktree|reset|restore|rebase|merge|cherry-pick|bisect)\b/;
|
|
58
|
+
/** Default horizontal pad when the launcher didn't forward one (matches outputPad=1). */
|
|
59
|
+
const DEFAULT_PAD_X = 1;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the status bar's left pad from the launcher's `YAGNI_PAD_X` env so it
|
|
62
|
+
* aligns with the editor input and the chat/output area on one shared column.
|
|
63
|
+
* The launcher seeds the same value as pi's `editorPaddingX`; this footer can't
|
|
64
|
+
* read pi settings, so the value crosses over env. Falls back to the output
|
|
65
|
+
* area's default (1) when unset/invalid. Clamped to pi's 0–3 editor range.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveFooterPadX(raw = process.env.YAGNI_PAD_X) {
|
|
68
|
+
const n = raw === undefined ? NaN : Number.parseInt(raw, 10);
|
|
69
|
+
return Number.isFinite(n) && n >= 0 && n <= 3 ? n : DEFAULT_PAD_X;
|
|
70
|
+
}
|
|
71
|
+
/** Format token counts for compact footer display (mirrors pi's formatTokens). */
|
|
72
|
+
export function formatTokens(count) {
|
|
73
|
+
if (count < 1000)
|
|
74
|
+
return count.toString();
|
|
75
|
+
if (count < 10000)
|
|
76
|
+
return `${(count / 1000).toFixed(1)}k`;
|
|
77
|
+
if (count < 1000000)
|
|
78
|
+
return `${Math.round(count / 1000)}k`;
|
|
79
|
+
if (count < 10000000)
|
|
80
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
81
|
+
return `${Math.round(count / 1000000)}M`;
|
|
82
|
+
}
|
|
83
|
+
/** Shorten cwd relative to home, like pi's built-in footer. */
|
|
84
|
+
export function formatCwd(cwd, home) {
|
|
85
|
+
if (!home)
|
|
86
|
+
return cwd;
|
|
87
|
+
const resolvedCwd = resolve(cwd);
|
|
88
|
+
const resolvedHome = resolve(home);
|
|
89
|
+
const rel = relative(resolvedHome, resolvedCwd);
|
|
90
|
+
const inside = rel === "" ||
|
|
91
|
+
(rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
92
|
+
return inside ? (rel === "" ? "~" : `~/${rel}`) : cwd;
|
|
93
|
+
}
|
|
94
|
+
/** Accumulate usage from all session entries (mirrors pi's built-in footer). */
|
|
95
|
+
export function collectUsage(sessionManager) {
|
|
96
|
+
const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
97
|
+
for (const entry of sessionManager.getEntries()) {
|
|
98
|
+
let u;
|
|
99
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
100
|
+
u = entry.message.usage;
|
|
101
|
+
}
|
|
102
|
+
else if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.usage) {
|
|
103
|
+
u = entry.message.usage;
|
|
104
|
+
}
|
|
105
|
+
else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
|
|
106
|
+
u = entry.usage;
|
|
107
|
+
}
|
|
108
|
+
if (u) {
|
|
109
|
+
totals.input += u.input;
|
|
110
|
+
totals.output += u.output;
|
|
111
|
+
totals.cacheRead += u.cacheRead;
|
|
112
|
+
totals.cacheWrite += u.cacheWrite;
|
|
113
|
+
totals.cost += u.cost.total;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return totals;
|
|
117
|
+
}
|
|
118
|
+
/** End-cut ellipsis truncation (ANSI-aware) so the branch prefix stays readable. */
|
|
119
|
+
function truncateEnd(text, maxWidth, ellipsis = "…") {
|
|
120
|
+
if (visibleWidth(text) <= maxWidth)
|
|
121
|
+
return text;
|
|
122
|
+
return truncateToWidth(text, maxWidth, ellipsis);
|
|
123
|
+
}
|
|
124
|
+
function runGit(args, cwd) {
|
|
125
|
+
try {
|
|
126
|
+
const r = spawnSync("git", ["--no-optional-locks", ...args], {
|
|
127
|
+
cwd,
|
|
128
|
+
encoding: "utf8",
|
|
129
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
130
|
+
});
|
|
131
|
+
if (r.error || r.status !== 0)
|
|
132
|
+
return null;
|
|
133
|
+
return r.stdout.trim();
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function gitRepoRoot(cwd) {
|
|
140
|
+
return runGit(["rev-parse", "--show-toplevel"], cwd) || null;
|
|
141
|
+
}
|
|
142
|
+
function gitBranch(repoRoot) {
|
|
143
|
+
const out = runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], repoRoot);
|
|
144
|
+
if (out)
|
|
145
|
+
return out;
|
|
146
|
+
// Detached HEAD, or repo with no commits yet.
|
|
147
|
+
return runGit(["rev-parse", "--verify", "HEAD"], repoRoot) ? "detached" : null;
|
|
148
|
+
}
|
|
149
|
+
function gitWorktrees(repoRoot) {
|
|
150
|
+
const out = runGit(["worktree", "list", "--porcelain"], repoRoot);
|
|
151
|
+
if (!out)
|
|
152
|
+
return [];
|
|
153
|
+
return out
|
|
154
|
+
.split("\n")
|
|
155
|
+
.filter((l) => l.startsWith("worktree "))
|
|
156
|
+
.map((l) => l.slice("worktree ".length).trim())
|
|
157
|
+
.filter(Boolean);
|
|
158
|
+
}
|
|
159
|
+
/** The MAIN repository root (stable across worktrees) — used for the folder name. */
|
|
160
|
+
function gitMainRepoRoot(repoRoot) {
|
|
161
|
+
const common = runGit(["rev-parse", "--git-common-dir"], repoRoot);
|
|
162
|
+
if (!common)
|
|
163
|
+
return repoRoot;
|
|
164
|
+
const abs = isAbsolute(common) ? common : join(repoRoot, common);
|
|
165
|
+
return basename(abs) === ".git" ? dirname(abs) : repoRoot;
|
|
166
|
+
}
|
|
167
|
+
/** Main checkout: .git is a directory. Linked worktree: .git is a "gitdir:" file. */
|
|
168
|
+
function isLinkedWorktree(repoRoot) {
|
|
169
|
+
try {
|
|
170
|
+
return statSync(join(repoRoot, ".git")).isFile();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const liveWorktreeProbes = { isLinkedWorktree, worktrees: gitWorktrees };
|
|
177
|
+
/**
|
|
178
|
+
* Show [worktree] only on a LINKED worktree (never the main checkout), when the repo has
|
|
179
|
+
* >1 worktree, AND the dir differs from the branch slug. The main checkout is the default
|
|
180
|
+
* context and earns no label; a [bracket] only disambiguates a secondary working tree.
|
|
181
|
+
*/
|
|
182
|
+
export function resolveWorktreeLabel(repoRoot, branch, probes = liveWorktreeProbes) {
|
|
183
|
+
if (!probes.isLinkedWorktree(repoRoot))
|
|
184
|
+
return null;
|
|
185
|
+
if (probes.worktrees(repoRoot).length < 2)
|
|
186
|
+
return null;
|
|
187
|
+
const currentDir = basename(repoRoot);
|
|
188
|
+
if (branch && currentDir === branch)
|
|
189
|
+
return null;
|
|
190
|
+
return truncateEnd(currentDir, WORKTREE_MAX_WIDTH);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Detect git info for a cwd, gracefully. Never throws: non-git folders, missing git
|
|
194
|
+
* binary, and corrupt repos all degrade to a safe partial/empty result.
|
|
195
|
+
*/
|
|
196
|
+
export function detectGitInfo(cwd, home) {
|
|
197
|
+
const root = gitRepoRoot(cwd);
|
|
198
|
+
if (!root) {
|
|
199
|
+
return { folder: formatCwd(cwd, home), inRepo: false, branch: null, worktree: null };
|
|
200
|
+
}
|
|
201
|
+
const branch = gitBranch(root);
|
|
202
|
+
const worktree = resolveWorktreeLabel(root, branch);
|
|
203
|
+
return { folder: basename(gitMainRepoRoot(root)), inRepo: true, branch, worktree };
|
|
204
|
+
}
|
|
205
|
+
const MODE_DISPLAY = {
|
|
206
|
+
auto: { text: "⏵⏵ auto mode", color: "accent" },
|
|
207
|
+
review: { text: "✓ review mode", color: "warning" },
|
|
208
|
+
plan: { text: "⏸ plan mode", color: "success" },
|
|
209
|
+
};
|
|
210
|
+
function modeDisplay(mode) {
|
|
211
|
+
return MODE_DISPLAY[mode];
|
|
212
|
+
}
|
|
213
|
+
/** Context color: success below 70, warning 70-90, error above 90. */
|
|
214
|
+
function contextColor(percent) {
|
|
215
|
+
if (percent === null)
|
|
216
|
+
return "success";
|
|
217
|
+
if (percent > 90)
|
|
218
|
+
return "error";
|
|
219
|
+
if (percent > 70)
|
|
220
|
+
return "warning";
|
|
221
|
+
return "success";
|
|
222
|
+
}
|
|
223
|
+
/** Pure line-builder, exported for tests. All data injected; colors via theme. */
|
|
224
|
+
export function renderFooterLines(input, theme, width, padX = 0) {
|
|
225
|
+
const dim = (s) => theme.fg("dim", s);
|
|
226
|
+
const sep = dim(SEP);
|
|
227
|
+
// Reserve the left pad so the status bar's text starts on the same column as
|
|
228
|
+
// the (padded) editor input and the chat/output area, instead of hugging the
|
|
229
|
+
// terminal edge. Truncation runs against the reduced content width.
|
|
230
|
+
const pad = " ".repeat(Math.max(0, Math.min(3, Math.floor(padX))));
|
|
231
|
+
const contentWidth = Math.max(1, width - pad.length);
|
|
232
|
+
// Line 1: folder · [worktree] · branch
|
|
233
|
+
const line1Parts = [theme.fg("accent", input.git.folder)];
|
|
234
|
+
if (input.git.inRepo) {
|
|
235
|
+
if (input.git.worktree)
|
|
236
|
+
line1Parts.push(theme.fg("warning", `[${input.git.worktree}]`));
|
|
237
|
+
if (input.git.branch)
|
|
238
|
+
line1Parts.push(theme.fg("border", truncateEnd(input.git.branch, BRANCH_MAX_WIDTH, dim("…"))));
|
|
239
|
+
}
|
|
240
|
+
const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
|
|
241
|
+
// Line 2: [mode ·] model · ↑in ↓out $cost · ctx%
|
|
242
|
+
const statParts = [];
|
|
243
|
+
if (input.usage.input)
|
|
244
|
+
statParts.push(`↑${formatTokens(input.usage.input)}`);
|
|
245
|
+
if (input.usage.output)
|
|
246
|
+
statParts.push(`↓${formatTokens(input.usage.output)}`);
|
|
247
|
+
if (input.usage.cost)
|
|
248
|
+
statParts.push(`$${input.usage.cost.toFixed(3)}`);
|
|
249
|
+
const stats = statParts.join(" ");
|
|
250
|
+
const percentText = input.contextPercent === null ? "?" : `${Math.round(input.contextPercent)}%`;
|
|
251
|
+
const line2Parts = [];
|
|
252
|
+
if (input.mode) {
|
|
253
|
+
const modeLabel = modeDisplay(input.mode);
|
|
254
|
+
line2Parts.push(theme.fg(modeLabel.color, modeLabel.text) + dim(" (shift+tab to change)"));
|
|
255
|
+
}
|
|
256
|
+
line2Parts.push(dim(input.model));
|
|
257
|
+
if (stats)
|
|
258
|
+
line2Parts.push(dim(stats));
|
|
259
|
+
line2Parts.push(theme.fg(contextColor(input.contextPercent), percentText));
|
|
260
|
+
const line2 = pad + truncateToWidth(line2Parts.join(sep), contentWidth, dim("…"));
|
|
261
|
+
// Line 3: extension statuses (branding, todo counter, mode), joined by " · ".
|
|
262
|
+
const statuses = input.statuses.map((s) => s.replace(/[\r\n\t]/g, " ").trim()).filter(Boolean);
|
|
263
|
+
const lines = [line1, line2];
|
|
264
|
+
if (statuses.length > 0) {
|
|
265
|
+
lines.push(pad + truncateToWidth(dim(statuses.join(SEP)), contentWidth, dim("…")));
|
|
266
|
+
}
|
|
267
|
+
return lines;
|
|
268
|
+
}
|
|
269
|
+
export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
270
|
+
return (_tui, theme, footerData) => {
|
|
271
|
+
let gitCache;
|
|
272
|
+
const unsubscribeBranch = footerData.onBranchChange?.(() => {
|
|
273
|
+
gitCache = undefined;
|
|
274
|
+
});
|
|
275
|
+
const gitInfo = () => {
|
|
276
|
+
if (!gitCache) {
|
|
277
|
+
gitCache = detectGitInfo(ctx.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
|
|
278
|
+
}
|
|
279
|
+
return gitCache;
|
|
280
|
+
};
|
|
281
|
+
if (invalidateHandle) {
|
|
282
|
+
invalidateHandle.invalidateGit = () => { gitCache = undefined; };
|
|
283
|
+
invalidateHandle.requestRender = () => { _tui?.requestRender?.(); };
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
render(width) {
|
|
287
|
+
const statuses = [...footerData.getExtensionStatuses().entries()]
|
|
288
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
289
|
+
.map(([, text]) => text);
|
|
290
|
+
return renderFooterLines({
|
|
291
|
+
git: gitInfo(),
|
|
292
|
+
model: ctx.model?.id ?? "no-model",
|
|
293
|
+
mode: modeHolder?.get() ?? null,
|
|
294
|
+
usage: collectUsage(ctx.sessionManager),
|
|
295
|
+
contextPercent: ctx.getContextUsage()?.percent ?? null,
|
|
296
|
+
statuses,
|
|
297
|
+
}, theme, width, resolveFooterPadX());
|
|
298
|
+
},
|
|
299
|
+
invalidate() {
|
|
300
|
+
gitCache = undefined;
|
|
301
|
+
},
|
|
302
|
+
dispose() {
|
|
303
|
+
unsubscribeBranch?.();
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=footer.js.map
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
import type { PipelineStage } from "./pipeline/types.js";
|
|
32
|
+
export type GuardianOutcome = "allow" | "ask" | "deny";
|
|
33
|
+
export type GuardianRiskLevel = "low" | "medium" | "high" | "critical";
|
|
34
|
+
export interface GuardianVerdict {
|
|
35
|
+
outcome: GuardianOutcome;
|
|
36
|
+
riskLevel: GuardianRiskLevel;
|
|
37
|
+
rationale: string;
|
|
38
|
+
}
|
|
39
|
+
export interface GuardianLimits {
|
|
40
|
+
/** Cap on Guardian reviews within the sliding window ({@link GUARDIAN_REVIEW_WINDOW_MS}). */
|
|
41
|
+
maxReviews: number;
|
|
42
|
+
/** Consecutive denials per turn before the circuit breaker trips. */
|
|
43
|
+
maxConsecutiveDenials: number;
|
|
44
|
+
/** Hard timeout for the Guardian consult in ms. */
|
|
45
|
+
timeoutMs: number;
|
|
46
|
+
}
|
|
47
|
+
export declare const DEFAULT_GUARDIAN_LIMITS: GuardianLimits;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve Guardian limits from the environment. `YAGNI_GUARDIAN_MAX_REVIEWS`
|
|
50
|
+
* overrides the sliding-window review cap; anything non-numeric or < 1 falls
|
|
51
|
+
* back to the default (a bad value must never zero out the cap and lock the
|
|
52
|
+
* session).
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveGuardianLimits(env?: Record<string, string | undefined>): GuardianLimits;
|
|
55
|
+
/** The model tier the Guardian runs on. Configurable via YAGNI_GUARDIAN_TIER. */
|
|
56
|
+
export declare const GUARDIAN_MODEL_TIER = "efficient";
|
|
57
|
+
/** Read-only tools — the Guardian can read files for context but cannot write or execute. */
|
|
58
|
+
export declare const GUARDIAN_TOOLS: string[];
|
|
59
|
+
/**
|
|
60
|
+
* The review-cap window. `reviews` counts consults inside a SLIDING window
|
|
61
|
+
* rather than for the session's lifetime: a 24/7 session (a fleet operator's
|
|
62
|
+
* always-on terminal) must regain review capacity as old consults age out,
|
|
63
|
+
* not hard-block forever after the first N. The cap is a cost/runaway bound,
|
|
64
|
+
* not a safety bound — safety is the verdicts themselves.
|
|
65
|
+
*/
|
|
66
|
+
export declare const GUARDIAN_REVIEW_WINDOW_MS: number;
|
|
67
|
+
export interface GuardianState {
|
|
68
|
+
/** Guardian consults within the last {@link GUARDIAN_REVIEW_WINDOW_MS}. */
|
|
69
|
+
reviews: number;
|
|
70
|
+
consecutiveDenials: number;
|
|
71
|
+
}
|
|
72
|
+
export interface GuardianStateHandle {
|
|
73
|
+
read(): GuardianState;
|
|
74
|
+
recordReview(outcome: GuardianOutcome): GuardianState;
|
|
75
|
+
resetTurn(): void;
|
|
76
|
+
}
|
|
77
|
+
export declare function makeGuardianState(now?: () => number): GuardianStateHandle;
|
|
78
|
+
export interface CircuitBreakerResult {
|
|
79
|
+
tripped: boolean;
|
|
80
|
+
reason?: string;
|
|
81
|
+
}
|
|
82
|
+
export declare function checkCircuitBreaker(state: GuardianState, limits: GuardianLimits): CircuitBreakerResult;
|
|
83
|
+
export declare function parseVerdict(raw: string): GuardianVerdict | null;
|
|
84
|
+
export declare function formatGuardianSubtotal(state: GuardianState, limits: GuardianLimits): string;
|
|
85
|
+
export type GuardianError = "timeout" | "malformed" | "network" | "empty" | "aborted";
|
|
86
|
+
export interface ReviewResult {
|
|
87
|
+
verdict: GuardianVerdict | null;
|
|
88
|
+
error?: GuardianError;
|
|
89
|
+
cost: number;
|
|
90
|
+
}
|
|
91
|
+
export interface ReviewCommandDeps {
|
|
92
|
+
runStage?: typeof defaultRunStage;
|
|
93
|
+
cwd: string;
|
|
94
|
+
signal?: AbortSignal;
|
|
95
|
+
/** Override the model tier (default: efficient). */
|
|
96
|
+
modelTier?: string;
|
|
97
|
+
/** Consult timeout in ms (default: DEFAULT_GUARDIAN_LIMITS.timeoutMs). */
|
|
98
|
+
timeoutMs?: number;
|
|
99
|
+
/**
|
|
100
|
+
* The exec policy's justification for routing this command to the Guardian
|
|
101
|
+
* ("pushes to remote — confirm intent"). Included in the consult prompt so
|
|
102
|
+
* an `ask` rationale can add information beyond the static rule text.
|
|
103
|
+
*/
|
|
104
|
+
execJustification?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The synthetic stage a Guardian consult runs as. Borrows the `plan` StageId
|
|
108
|
+
* (same pattern as the advisor) so it doesn't ripple into feed/reducers. The
|
|
109
|
+
* agent name selects the guardian persona from PERSONA_BODIES.
|
|
110
|
+
*/
|
|
111
|
+
export declare function guardianStage(modelTier?: string): PipelineStage;
|
|
112
|
+
/**
|
|
113
|
+
* Run a Guardian consult: spawn a locked-down child pi with the risk policy
|
|
114
|
+
* persona and the command as the task. Parse the JSON verdict from the output.
|
|
115
|
+
* Returns { verdict, cost } on success, { verdict: null, error, cost } on failure.
|
|
116
|
+
*/
|
|
117
|
+
export declare function reviewCommand(command: string, deps: ReviewCommandDeps): Promise<ReviewResult>;
|
|
118
|
+
export interface GuardianDiagnosticEvent {
|
|
119
|
+
event: "guardian_review";
|
|
120
|
+
outcome: GuardianOutcome | GuardianError;
|
|
121
|
+
durationMs?: number;
|
|
122
|
+
tier?: string;
|
|
123
|
+
/** Debug-only: command hash for correlation (never the raw command). */
|
|
124
|
+
commandHash?: string;
|
|
125
|
+
/** Debug-only: the Guardian's rationale. */
|
|
126
|
+
rationale?: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Create a sanitized diagnostic event. Never includes the raw command text
|
|
130
|
+
* (could contain secrets). YAGNI_DEBUG=1 adds rationale and a command hash.
|
|
131
|
+
*/
|
|
132
|
+
export declare function buildDiagnosticEvent(outcome: GuardianOutcome | GuardianError, opts: {
|
|
133
|
+
durationMs?: number;
|
|
134
|
+
tier?: string;
|
|
135
|
+
rationale?: string;
|
|
136
|
+
commandHash?: string;
|
|
137
|
+
debug?: boolean;
|
|
138
|
+
}): GuardianDiagnosticEvent;
|
|
139
|
+
//# sourceMappingURL=guardian.d.ts.map
|