@yagni-app/code-staging 0.0.0 → 0.1.0-staging.997.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/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +188 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +168 -0
- package/dist/extension/pipeline/eval.js +182 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +636 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +51 -0
- package/dist/extension/pipeline/stages.js +113 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +135 -0
- package/dist/extension/subagents.js +281 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +39 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration + catalog-fetch helpers for the YAGNI pi extension.
|
|
3
|
+
*
|
|
4
|
+
* `YAGNI_BASE_URL` and `YAGNI_TOKEN` are read from the process environment,
|
|
5
|
+
* which the `yagni` launcher is responsible for populating. `YAGNI_CODE_DEBUG`
|
|
6
|
+
* (any truthy value) is an optional operator switch that surfaces fail-soft
|
|
7
|
+
* diagnostics on stderr, e.g. why a /go run could not be recorded (HTTP status +
|
|
8
|
+
* a token-free body excerpt); off by default and never required.
|
|
9
|
+
*/
|
|
10
|
+
import { resilientFetch } from "./resilientFetch.js";
|
|
11
|
+
const DEFAULT_BASE_URL = "https://yagni.app";
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the YAGNI backend base URL from the environment.
|
|
14
|
+
*
|
|
15
|
+
* Falls back to {@link DEFAULT_BASE_URL}, trims whitespace, and strips a single
|
|
16
|
+
* trailing slash so callers can safely append paths.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveBaseUrl(env = process.env) {
|
|
19
|
+
return (env.YAGNI_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
20
|
+
}
|
|
21
|
+
/** Read the bearer token used to authenticate with the YAGNI backend. */
|
|
22
|
+
export function getToken(env = process.env) {
|
|
23
|
+
return env.YAGNI_TOKEN;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read the workspace id the token is bound to, forwarded by the launcher as
|
|
27
|
+
* `YAGNI_WORKSPACE_ID` (from the stored credentials). Used to key the one-time
|
|
28
|
+
* init-pass marker ({@link initDone}). Absent on older logins (treated as
|
|
29
|
+
* unknown → the marker gate degrades to freshness-only, still fail-closed).
|
|
30
|
+
*/
|
|
31
|
+
export function getWorkspaceId(env = process.env) {
|
|
32
|
+
return env.YAGNI_WORKSPACE_ID?.trim() || undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read the token's ISO expiry, forwarded by the launcher as
|
|
36
|
+
* `YAGNI_TOKEN_EXPIRES_AT`. Absent on older logins (treated as unknown).
|
|
37
|
+
*/
|
|
38
|
+
export function getTokenExpiresAt(env = process.env) {
|
|
39
|
+
return env.YAGNI_TOKEN_EXPIRES_AT;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* How close to expiry the token can be before the session-start notice fires.
|
|
43
|
+
* A long session can outlive its token, so a single heads-up lets the user
|
|
44
|
+
* re-login before completions and grounding tools start 401-ing mid-flight.
|
|
45
|
+
*/
|
|
46
|
+
export const EXPIRY_NOTICE_THRESHOLD_MS = 30 * 60 * 1000;
|
|
47
|
+
/**
|
|
48
|
+
* Pure helper: given the token's ISO expiry and the current time, return the
|
|
49
|
+
* notice to surface at session start, or null when there is nothing to say
|
|
50
|
+
* (missing/unparseable expiry, or comfortably valid). Never throws. This only
|
|
51
|
+
* reads the expiry; it never refreshes the token (no new auth surface).
|
|
52
|
+
*/
|
|
53
|
+
export function tokenExpiryNotice(expiresAt, nowMs, thresholdMs = EXPIRY_NOTICE_THRESHOLD_MS) {
|
|
54
|
+
if (!expiresAt)
|
|
55
|
+
return null;
|
|
56
|
+
const expiryMs = Date.parse(expiresAt);
|
|
57
|
+
if (Number.isNaN(expiryMs))
|
|
58
|
+
return null;
|
|
59
|
+
const remaining = expiryMs - nowMs;
|
|
60
|
+
if (remaining <= 0) {
|
|
61
|
+
return {
|
|
62
|
+
message: "Your YAGNI Code session has expired. Run `yagni login` to re-authenticate.",
|
|
63
|
+
type: "error",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (remaining <= thresholdMs) {
|
|
67
|
+
const minutes = Math.max(1, Math.ceil(remaining / 60_000));
|
|
68
|
+
return {
|
|
69
|
+
message: `Your YAGNI Code session expires in about ${minutes} minute${minutes === 1 ? "" : "s"}. ` +
|
|
70
|
+
"Run `yagni login` to refresh it before it lapses.",
|
|
71
|
+
type: "warning",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fetch the YAGNI model catalog at startup.
|
|
78
|
+
*
|
|
79
|
+
* @throws an actionable Error (mentioning `yagni login`) on any non-2xx
|
|
80
|
+
* response so the launcher can surface a clear re-authentication prompt.
|
|
81
|
+
*/
|
|
82
|
+
export async function fetchCatalog(opts) {
|
|
83
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/models`, { method: "GET", headers: { authorization: `Bearer ${opts.getToken() ?? ""}` } }, { fetchImpl: opts.fetchImpl });
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
throw new Error(`Failed to fetch YAGNI model catalog (HTTP ${res.status}). Run \`yagni login\` to re-authenticate.`);
|
|
86
|
+
}
|
|
87
|
+
const data = (await res.json());
|
|
88
|
+
return data.models;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Fetch the workspace company brief at startup.
|
|
92
|
+
*
|
|
93
|
+
* Unlike {@link fetchCatalog}, this is BEST-EFFORT: a missing or failed brief
|
|
94
|
+
* must never block the agent (it still works, just without boot-time grounding).
|
|
95
|
+
* Returns `null` on any non-2xx response or network error.
|
|
96
|
+
*/
|
|
97
|
+
export async function fetchContextBrief(opts) {
|
|
98
|
+
try {
|
|
99
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/context`, { method: "GET", headers: { authorization: `Bearer ${opts.getToken() ?? ""}` } }, { fetchImpl: opts.fetchImpl });
|
|
100
|
+
if (!res.ok)
|
|
101
|
+
return null;
|
|
102
|
+
return (await res.json());
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2 - /cost command + a session usage accumulator.
|
|
3
|
+
*
|
|
4
|
+
* pi already shows model + usage in its footer, so this does not rebuild a live
|
|
5
|
+
* counter. The net-new value is a pull `/cost` command backed by a pure,
|
|
6
|
+
* session-scoped accumulator and an optional, fail-soft credits-headroom fetch, so
|
|
7
|
+
* invisible enforcement becomes visible trust.
|
|
8
|
+
*
|
|
9
|
+
* Feasibility correction (verified against pi 0.80.2): the interactive
|
|
10
|
+
* `after_provider_response` event carries only { status, headers }, NOT usage. The
|
|
11
|
+
* usage-bearing seam is `turn_end`, whose `message: AgentMessage` carries `usage`
|
|
12
|
+
* (the same shape events.ts already reads off the NDJSON stream). So the
|
|
13
|
+
* accumulator hangs off `turn_end`, not `after_provider_response`.
|
|
14
|
+
*
|
|
15
|
+
* The accumulator + formatter are PURE; the headroom fetch is injectable and
|
|
16
|
+
* fail-soft, so /cost always reports session usage even when the backend is down.
|
|
17
|
+
*/
|
|
18
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
/** A single turn's usage delta, normalized from pi's message.usage shape. */
|
|
20
|
+
export interface UsageDelta {
|
|
21
|
+
input: number;
|
|
22
|
+
output: number;
|
|
23
|
+
cacheRead: number;
|
|
24
|
+
cacheWrite: number;
|
|
25
|
+
cost: number;
|
|
26
|
+
}
|
|
27
|
+
/** Running session totals (turns = number of assistant turns accumulated). */
|
|
28
|
+
export interface CostSnapshot extends UsageDelta {
|
|
29
|
+
turns: number;
|
|
30
|
+
}
|
|
31
|
+
export interface CostAccumulator {
|
|
32
|
+
add(delta: UsageDelta): void;
|
|
33
|
+
snapshot(): CostSnapshot;
|
|
34
|
+
reset(): void;
|
|
35
|
+
}
|
|
36
|
+
/** A pure, session-scoped usage accumulator. */
|
|
37
|
+
export declare function makeCostAccumulator(): CostAccumulator;
|
|
38
|
+
/** Extract a normalized {@link UsageDelta} from a turn_end message, defensively. */
|
|
39
|
+
export declare function usageFromMessage(message: unknown): UsageDelta;
|
|
40
|
+
/** Credit headroom returned by the backend (fail-soft; null when unavailable). */
|
|
41
|
+
export interface Headroom {
|
|
42
|
+
remaining: number;
|
|
43
|
+
unit: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Render the /cost line. Pure, no em-dashes. `headroom` null means it was
|
|
47
|
+
* unavailable. `advisorLine` is the ask_advisor subtotal (empty when the session
|
|
48
|
+
* never escalated).
|
|
49
|
+
*
|
|
50
|
+
* HONESTY NOTE: this counter can only see the DRIVER session, because it is fed
|
|
51
|
+
* by pi's `turn_end`. Work that runs in a child process — every /go stage, and
|
|
52
|
+
* every advisor consult — never emits a parent `turn_end`, so it is invisible
|
|
53
|
+
* here. The advisor subtotal is threaded in explicitly for exactly that reason.
|
|
54
|
+
* /go's spend is still missing; the server-authoritative fix is YAG-383.
|
|
55
|
+
*/
|
|
56
|
+
export declare function formatCostLine(snap: CostSnapshot, headroom?: Headroom | null, advisorLine?: string): string;
|
|
57
|
+
export interface RegisterCostDeps {
|
|
58
|
+
/** Fetch remaining credit headroom; return null when unavailable. Fail-soft. */
|
|
59
|
+
fetchHeadroom?: (signal?: AbortSignal) => Promise<Headroom | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Render the ask_advisor subtotal for this session, or "" when it never
|
|
62
|
+
* escalated. Threaded in because advisor consults run in a child process and
|
|
63
|
+
* so never reach the `turn_end` accumulator.
|
|
64
|
+
*/
|
|
65
|
+
advisorSubtotal?: () => string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Wire the turn_end accumulator + the /cost command. /cost reports the session
|
|
69
|
+
* totals plus headroom (when the fetch resolves); a failing or absent fetch still
|
|
70
|
+
* reports session usage. `/cost reset` zeroes the session counter.
|
|
71
|
+
*/
|
|
72
|
+
export declare function registerCostCommand(pi: ExtensionAPI, deps?: RegisterCostDeps): void;
|
|
73
|
+
//# sourceMappingURL=costHud.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2 - /cost command + a session usage accumulator.
|
|
3
|
+
*
|
|
4
|
+
* pi already shows model + usage in its footer, so this does not rebuild a live
|
|
5
|
+
* counter. The net-new value is a pull `/cost` command backed by a pure,
|
|
6
|
+
* session-scoped accumulator and an optional, fail-soft credits-headroom fetch, so
|
|
7
|
+
* invisible enforcement becomes visible trust.
|
|
8
|
+
*
|
|
9
|
+
* Feasibility correction (verified against pi 0.80.2): the interactive
|
|
10
|
+
* `after_provider_response` event carries only { status, headers }, NOT usage. The
|
|
11
|
+
* usage-bearing seam is `turn_end`, whose `message: AgentMessage` carries `usage`
|
|
12
|
+
* (the same shape events.ts already reads off the NDJSON stream). So the
|
|
13
|
+
* accumulator hangs off `turn_end`, not `after_provider_response`.
|
|
14
|
+
*
|
|
15
|
+
* The accumulator + formatter are PURE; the headroom fetch is injectable and
|
|
16
|
+
* fail-soft, so /cost always reports session usage even when the backend is down.
|
|
17
|
+
*/
|
|
18
|
+
/** A pure, session-scoped usage accumulator. */
|
|
19
|
+
export function makeCostAccumulator() {
|
|
20
|
+
let s = { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
21
|
+
return {
|
|
22
|
+
add(d) {
|
|
23
|
+
s = {
|
|
24
|
+
turns: s.turns + 1,
|
|
25
|
+
input: s.input + d.input,
|
|
26
|
+
output: s.output + d.output,
|
|
27
|
+
cacheRead: s.cacheRead + d.cacheRead,
|
|
28
|
+
cacheWrite: s.cacheWrite + d.cacheWrite,
|
|
29
|
+
cost: s.cost + d.cost,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
snapshot() {
|
|
33
|
+
return { ...s };
|
|
34
|
+
},
|
|
35
|
+
reset() {
|
|
36
|
+
s = { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Extract a normalized {@link UsageDelta} from a turn_end message, defensively. */
|
|
41
|
+
export function usageFromMessage(message) {
|
|
42
|
+
const u = message?.usage;
|
|
43
|
+
if (!u)
|
|
44
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
45
|
+
const cost = typeof u.cost === "number" ? u.cost : (u.cost?.total ?? 0);
|
|
46
|
+
return {
|
|
47
|
+
input: u.input ?? 0,
|
|
48
|
+
output: u.output ?? 0,
|
|
49
|
+
cacheRead: u.cacheRead ?? 0,
|
|
50
|
+
cacheWrite: u.cacheWrite ?? 0,
|
|
51
|
+
cost,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
55
|
+
/**
|
|
56
|
+
* Render the /cost line. Pure, no em-dashes. `headroom` null means it was
|
|
57
|
+
* unavailable. `advisorLine` is the ask_advisor subtotal (empty when the session
|
|
58
|
+
* never escalated).
|
|
59
|
+
*
|
|
60
|
+
* HONESTY NOTE: this counter can only see the DRIVER session, because it is fed
|
|
61
|
+
* by pi's `turn_end`. Work that runs in a child process — every /go stage, and
|
|
62
|
+
* every advisor consult — never emits a parent `turn_end`, so it is invisible
|
|
63
|
+
* here. The advisor subtotal is threaded in explicitly for exactly that reason.
|
|
64
|
+
* /go's spend is still missing; the server-authoritative fix is YAG-383.
|
|
65
|
+
*/
|
|
66
|
+
export function formatCostLine(snap, headroom, advisorLine) {
|
|
67
|
+
const turns = `${snap.turns} turn${snap.turns === 1 ? "" : "s"}`;
|
|
68
|
+
const cached = snap.cacheRead > 0 ? ` (${fmt(snap.cacheRead)} cached)` : "";
|
|
69
|
+
const base = `Session usage: ${turns}, ${fmt(snap.input)} in / ${fmt(snap.output)} out tokens${cached}, ` +
|
|
70
|
+
`$${snap.cost.toFixed(2)} this session.`;
|
|
71
|
+
const advisor = advisorLine?.trim() ? ` ${advisorLine.trim()}` : "";
|
|
72
|
+
if (headroom)
|
|
73
|
+
return `${base}${advisor} Credit headroom: ${headroom.remaining} ${headroom.unit}.`;
|
|
74
|
+
if (headroom === null)
|
|
75
|
+
return `${base}${advisor} Credit headroom unavailable right now.`;
|
|
76
|
+
return `${base}${advisor}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Wire the turn_end accumulator + the /cost command. /cost reports the session
|
|
80
|
+
* totals plus headroom (when the fetch resolves); a failing or absent fetch still
|
|
81
|
+
* reports session usage. `/cost reset` zeroes the session counter.
|
|
82
|
+
*/
|
|
83
|
+
export function registerCostCommand(pi, deps = {}) {
|
|
84
|
+
const acc = makeCostAccumulator();
|
|
85
|
+
const fetchHeadroom = deps.fetchHeadroom;
|
|
86
|
+
pi.on("turn_end", (event) => {
|
|
87
|
+
try {
|
|
88
|
+
acc.add(usageFromMessage(event.message));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* usage accounting must never disrupt a turn */
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
pi.registerCommand("cost", {
|
|
95
|
+
description: "Show this session's token + cost usage, and remaining credit headroom. /cost reset to zero it.",
|
|
96
|
+
handler: async (args, ctx) => {
|
|
97
|
+
if (args.trim().toLowerCase() === "reset") {
|
|
98
|
+
acc.reset();
|
|
99
|
+
if (ctx.hasUI)
|
|
100
|
+
ctx.ui.notify("Session usage counter reset.", "info");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
let headroom = undefined;
|
|
104
|
+
if (fetchHeadroom) {
|
|
105
|
+
try {
|
|
106
|
+
headroom = await fetchHeadroom(ctx.signal);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
headroom = null; // fail-soft: still report session usage
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
let advisorLine = "";
|
|
113
|
+
try {
|
|
114
|
+
advisorLine = deps.advisorSubtotal?.() ?? "";
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* usage accounting must never break /cost */
|
|
118
|
+
}
|
|
119
|
+
await pi.sendUserMessage(formatCostLine(acc.snapshot(), headroom, advisorLine));
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=costHud.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision capture at the human gate (spec §4, M3 — conservative).
|
|
3
|
+
*
|
|
4
|
+
* When the user picks "Yes, and don't ask again for <tool> in <dir>" at the
|
|
5
|
+
* review gate, that is a piece of durable judgment worth banking so YAGNI
|
|
6
|
+
* answers the same question next time instead of interrupting a human. Capture
|
|
7
|
+
* is deliberately conservative:
|
|
8
|
+
*
|
|
9
|
+
* - HEADLESS never auto-banks (no consent surface; review-mode bless is itself
|
|
10
|
+
* fail-closed headless, but this guards regardless).
|
|
11
|
+
* - TUI ALWAYS confirms via `ctx.ui.confirm` before banking.
|
|
12
|
+
* - DEBOUNCED to at most one capture PROMPT per 10 minutes, so a burst of
|
|
13
|
+
* blesses never nags.
|
|
14
|
+
*
|
|
15
|
+
* The bank goes through {@link bankDecision} (source `cli_correction`,
|
|
16
|
+
* idempotency-keyed, spool-durable). Everything is fail-soft: a capture problem
|
|
17
|
+
* never affects the tool call that triggered it.
|
|
18
|
+
*/
|
|
19
|
+
import { type DecisionClientOpts } from "./decisions.js";
|
|
20
|
+
import type { JudgmentWriteOutcome } from "./spool.js";
|
|
21
|
+
/** At most one capture prompt per this window (spec: 10 minutes). */
|
|
22
|
+
export declare const CAPTURE_DEBOUNCE_MS: number;
|
|
23
|
+
/** The minimal context the capture needs (a subset of pi's ExtensionContext). */
|
|
24
|
+
export interface CaptureCtx {
|
|
25
|
+
hasUI: boolean;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
ui: {
|
|
28
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
29
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** What was blessed: the tool and a human-readable directory label. */
|
|
33
|
+
export interface BlessCaptureInfo {
|
|
34
|
+
tool: string;
|
|
35
|
+
dir: string;
|
|
36
|
+
}
|
|
37
|
+
export interface DecisionCapture {
|
|
38
|
+
/** On a bless-with-remember: debounced, TUI-confirmed decision capture. */
|
|
39
|
+
captureFromBless(ctx: CaptureCtx, info: BlessCaptureInfo): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/** Injectable seams for {@link makeDecisionCapture}. */
|
|
42
|
+
export interface DecisionCaptureDeps extends DecisionClientOpts {
|
|
43
|
+
now?: () => number;
|
|
44
|
+
/** Bank implementation (default: {@link bankDecision}); injected in tests. */
|
|
45
|
+
bank?: (opts: DecisionClientOpts, params: {
|
|
46
|
+
question: string;
|
|
47
|
+
decision: string;
|
|
48
|
+
}, signal?: AbortSignal) => Promise<JudgmentWriteOutcome>;
|
|
49
|
+
}
|
|
50
|
+
/** Build a session-scoped decision capture (holds the debounce timestamp). */
|
|
51
|
+
export declare function makeDecisionCapture(deps: DecisionCaptureDeps): DecisionCapture;
|
|
52
|
+
//# sourceMappingURL=decisionCapture.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision capture at the human gate (spec §4, M3 — conservative).
|
|
3
|
+
*
|
|
4
|
+
* When the user picks "Yes, and don't ask again for <tool> in <dir>" at the
|
|
5
|
+
* review gate, that is a piece of durable judgment worth banking so YAGNI
|
|
6
|
+
* answers the same question next time instead of interrupting a human. Capture
|
|
7
|
+
* is deliberately conservative:
|
|
8
|
+
*
|
|
9
|
+
* - HEADLESS never auto-banks (no consent surface; review-mode bless is itself
|
|
10
|
+
* fail-closed headless, but this guards regardless).
|
|
11
|
+
* - TUI ALWAYS confirms via `ctx.ui.confirm` before banking.
|
|
12
|
+
* - DEBOUNCED to at most one capture PROMPT per 10 minutes, so a burst of
|
|
13
|
+
* blesses never nags.
|
|
14
|
+
*
|
|
15
|
+
* The bank goes through {@link bankDecision} (source `cli_correction`,
|
|
16
|
+
* idempotency-keyed, spool-durable). Everything is fail-soft: a capture problem
|
|
17
|
+
* never affects the tool call that triggered it.
|
|
18
|
+
*/
|
|
19
|
+
import { bankDecision } from "./decisions.js";
|
|
20
|
+
/** At most one capture prompt per this window (spec: 10 minutes). */
|
|
21
|
+
export const CAPTURE_DEBOUNCE_MS = 10 * 60 * 1000;
|
|
22
|
+
/** Build a session-scoped decision capture (holds the debounce timestamp). */
|
|
23
|
+
export function makeDecisionCapture(deps) {
|
|
24
|
+
const now = deps.now ?? (() => Date.now());
|
|
25
|
+
const bank = deps.bank ?? bankDecision;
|
|
26
|
+
let lastPromptAt = Number.NEGATIVE_INFINITY;
|
|
27
|
+
return {
|
|
28
|
+
async captureFromBless(ctx, info) {
|
|
29
|
+
// Headless never auto-banks: there is no consent surface.
|
|
30
|
+
if (!ctx.hasUI)
|
|
31
|
+
return;
|
|
32
|
+
// Debounce: hold if we prompted within the window (whatever the answer).
|
|
33
|
+
const t = now();
|
|
34
|
+
if (t - lastPromptAt < CAPTURE_DEBOUNCE_MS)
|
|
35
|
+
return;
|
|
36
|
+
lastPromptAt = t;
|
|
37
|
+
const question = `Can the agent run ${info.tool} within ${info.dir} without asking each time?`;
|
|
38
|
+
const decision = `Yes, ${info.tool} within ${info.dir} is approved for this repository.`;
|
|
39
|
+
let ok = false;
|
|
40
|
+
try {
|
|
41
|
+
ok = await ctx.ui.confirm("Record this as a YAGNI decision?", `You approved ${info.tool} in ${info.dir}. Record it so YAGNI answers the same question next time instead of asking a human?`);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return; // a failed prompt never banks
|
|
45
|
+
}
|
|
46
|
+
if (!ok)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
const outcome = await bank(deps, { question, decision }, ctx.signal);
|
|
50
|
+
if (outcome.kind === "ok") {
|
|
51
|
+
ctx.ui.notify("Recorded the decision in YAGNI.", "info");
|
|
52
|
+
}
|
|
53
|
+
else if (outcome.kind === "spooled") {
|
|
54
|
+
ctx.ui.notify("Saved the decision locally; it will sync automatically.", "info");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
ctx.ui.notify(outcome.message, "error");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* fail-soft: a capture failure never affects the tool call */
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=decisionCapture.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client-side decisions surface (spec §4, W4): the `/decide` and
|
|
3
|
+
* `/decisions` commands plus the shared write helper the bless-capture flow
|
|
4
|
+
* reuses.
|
|
5
|
+
*
|
|
6
|
+
* - `bankDecision` POSTs a `source: 'cli_correction'` decision with a generated
|
|
7
|
+
* idempotencyKey through the durable write-spool (part A), so a transport
|
|
8
|
+
* failure or 5xx spools it for replay instead of losing it, and a replay can
|
|
9
|
+
* never double-bank (server dedups on the key).
|
|
10
|
+
* - `fetchDecisions` / `supersedeDecision` reuse the shared authed-fetch seam,
|
|
11
|
+
* so a GET/PATCH inherits the one 401-refresh-then-retry-once behaviour.
|
|
12
|
+
* - `/decide <question> => <decision>` records ONE decision directly (the user
|
|
13
|
+
* typed it — explicit intent, no extra confirm or debounce).
|
|
14
|
+
* - `/decisions` lists recent decisions (id-prefixed, compact); `/decisions
|
|
15
|
+
* supersede <id>` flips one to superseded.
|
|
16
|
+
*
|
|
17
|
+
* Formatters and the arg parser are pure; the network calls are injectable via
|
|
18
|
+
* the shared `SpoolClientOpts` so tests never touch disk or the network.
|
|
19
|
+
*/
|
|
20
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { type JudgmentWriteOutcome, type SpoolClientOpts } from "./spool.js";
|
|
22
|
+
/** Transport + idempotency seams for the decisions surface. */
|
|
23
|
+
export interface DecisionClientOpts extends SpoolClientOpts {
|
|
24
|
+
/** Idempotency-key source (default: crypto.randomUUID); injected in tests. */
|
|
25
|
+
makeIdempotencyKey?: () => string;
|
|
26
|
+
}
|
|
27
|
+
/** The durable fields banked by `/decide` or a bless-with-remember capture. */
|
|
28
|
+
export interface BankDecisionParams {
|
|
29
|
+
question: string;
|
|
30
|
+
decision: string;
|
|
31
|
+
rationale?: string;
|
|
32
|
+
repo?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Bank a `cli_correction` decision durably. Every logical write carries a
|
|
36
|
+
* generated idempotencyKey; the backend dedups on it, so a spool replay or a
|
|
37
|
+
* 401 retry can never bank the same decision twice. Transport failures and 5xx
|
|
38
|
+
* are spooled for replay instead of lost (R4 write half). Never throws.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bankDecision(opts: DecisionClientOpts, params: BankDecisionParams, signal?: AbortSignal): Promise<JudgmentWriteOutcome>;
|
|
41
|
+
/** One row from GET /decisions (mirrors the backend DecisionListItem). */
|
|
42
|
+
export interface DecisionListItem {
|
|
43
|
+
id: string;
|
|
44
|
+
question: string;
|
|
45
|
+
decision: string;
|
|
46
|
+
rationale: string | null;
|
|
47
|
+
repoFullName: string | null;
|
|
48
|
+
source: string;
|
|
49
|
+
status: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
citedCount: number;
|
|
52
|
+
}
|
|
53
|
+
/** Options for {@link fetchDecisions}. */
|
|
54
|
+
export interface FetchDecisionsOpts {
|
|
55
|
+
limit?: number;
|
|
56
|
+
repo?: string;
|
|
57
|
+
}
|
|
58
|
+
export declare const DEFAULT_LIST_LIMIT = 20;
|
|
59
|
+
/** GET recent decisions (any read scope). Throws a friendly error on non-2xx. */
|
|
60
|
+
export declare function fetchDecisions(opts: DecisionClientOpts, list?: FetchDecisionsOpts, signal?: AbortSignal): Promise<DecisionListItem[]>;
|
|
61
|
+
/**
|
|
62
|
+
* PATCH one decision to `superseded` (scope yagni_code:write). Idempotent flip,
|
|
63
|
+
* so a single attempt is enough — no client retry (invariant §0.8). Throws a
|
|
64
|
+
* friendly error on non-2xx.
|
|
65
|
+
*/
|
|
66
|
+
export declare function supersedeDecision(opts: DecisionClientOpts, id: string, supersededBy?: string, signal?: AbortSignal): Promise<void>;
|
|
67
|
+
/** The parsed `/decide` argument: `<question> => <decision>`. */
|
|
68
|
+
export interface DecideArgs {
|
|
69
|
+
question: string;
|
|
70
|
+
decision: string;
|
|
71
|
+
}
|
|
72
|
+
/** Pure: parse `<question> => <decision>`; null when either half is missing. */
|
|
73
|
+
export declare function parseDecideArgs(raw: string): DecideArgs | null;
|
|
74
|
+
/** Pure: a short id prefix for compact display. */
|
|
75
|
+
export declare function shortId(id: string): string;
|
|
76
|
+
/** Pure: render the compact, id-prefixed decisions list (no em-dashes). */
|
|
77
|
+
export declare function formatDecisionsList(items: DecisionListItem[]): string;
|
|
78
|
+
/**
|
|
79
|
+
* Wire the `/decide` and `/decisions` commands. Both are fail-soft: any
|
|
80
|
+
* transport error surfaces as an honest notice, never a thrown command.
|
|
81
|
+
*/
|
|
82
|
+
export declare function registerDecisionCommands(pi: ExtensionAPI, opts: DecisionClientOpts): void;
|
|
83
|
+
//# sourceMappingURL=decisions.d.ts.map
|