@yagni-app/code 0.1.0 → 0.2.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/README.md +65 -11
- package/dist/claudeCompat.d.ts +109 -0
- package/dist/claudeCompat.js +260 -0
- package/dist/claudePlugins.d.ts +109 -0
- package/dist/claudePlugins.js +336 -0
- package/dist/cli.d.ts +1 -5
- package/dist/cli.js +79 -20
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -2
- package/dist/crashReport.d.ts +135 -0
- package/dist/crashReport.js +291 -0
- package/dist/credentials.js +2 -1
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.js +81 -3
- 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 +194 -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 +87 -0
- package/dist/extension/boostCommand.d.ts +144 -0
- package/dist/extension/boostCommand.js +263 -0
- package/dist/extension/branding.d.ts +79 -0
- package/dist/extension/branding.js +156 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +373 -0
- package/dist/extension/claudeRules.d.ts +54 -0
- package/dist/extension/claudeRules.js +180 -0
- package/dist/extension/config.d.ts +173 -0
- package/dist/extension/config.js +194 -0
- package/dist/extension/costHud.d.ts +186 -0
- package/dist/extension/costHud.js +293 -0
- package/dist/extension/crashReport.d.ts +89 -0
- package/dist/extension/crashReport.js +241 -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 +155 -0
- package/dist/extension/index.js +597 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +170 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/mcpTools.d.ts +57 -0
- package/dist/extension/mcpTools.js +132 -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 +205 -0
- package/dist/extension/pipeline/eval.js +226 -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 +146 -0
- package/dist/extension/pipeline/goCommand.js +1085 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
- package/dist/extension/pipeline/goCompareCommand.js +203 -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 +645 -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/runCostTable.d.ts +37 -0
- package/dist/extension/pipeline/runCostTable.js +165 -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 +177 -0
- package/dist/extension/pipeline/runState.js +275 -0
- package/dist/extension/pipeline/runner.d.ts +98 -0
- package/dist/extension/pipeline/runner.js +310 -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 +53 -0
- package/dist/extension/pipeline/stages.js +115 -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 +439 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +763 -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 +202 -0
- package/dist/extension/provider.d.ts +17 -0
- package/dist/extension/provider.js +33 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +190 -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/rerouteNotice.d.ts +34 -0
- package/dist/extension/rerouteNotice.js +79 -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/sessionRuns.d.ts +45 -0
- package/dist/extension/sessionRuns.js +77 -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 +145 -0
- package/dist/extension/subagents.js +326 -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 +25 -3
- package/dist/launch.js +27 -9
- package/dist/login.d.ts +7 -0
- package/dist/login.js +3 -1
- package/dist/paths.d.ts +13 -4
- package/dist/paths.js +17 -5
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +5 -2
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +11 -15
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash reporting for the pi extension: a sanitized, fire-and-forget POST to
|
|
3
|
+
* the YAGNI backend's /api/yagni-code/crash. No third-party telemetry SDK —
|
|
4
|
+
* crash reports are the only thing that phones home, they contain no code and
|
|
5
|
+
* no prompts, and `YAGNI_DISABLE_CRASH_REPORTS=1` turns them off.
|
|
6
|
+
*
|
|
7
|
+
* Two report paths use this module:
|
|
8
|
+
* - `uncaughtExceptionMonitor` (installed by index.ts): the MONITOR hook is
|
|
9
|
+
* chosen deliberately — unlike an `uncaughtException` listener it never
|
|
10
|
+
* changes crash semantics in pi's process (which YAGNI does not own); it
|
|
11
|
+
* only observes the crash that is already happening. This is also the
|
|
12
|
+
* "tool error that indicates a crash, not a user error" discriminator:
|
|
13
|
+
* a user-visible tool failure is caught and rendered by pi, while
|
|
14
|
+
* anything reaching the monitor was about to kill the session.
|
|
15
|
+
* - the /go pipeline's terminal catch (goCommand.ts), where a thrown error
|
|
16
|
+
* — not a verdict-shaped failure — ends the run.
|
|
17
|
+
*
|
|
18
|
+
* Everything is fail-soft: never throws, never retries, single short-timeout
|
|
19
|
+
* attempt, and reporting can never affect the user's session.
|
|
20
|
+
*
|
|
21
|
+
* The sanitizer (env-value redaction + path collapse + caps around the shared
|
|
22
|
+
* scrubSecrets patterns) is kept in sync with
|
|
23
|
+
* `yagni-code-cli/src/crashReport.ts`; the backend re-runs its own copy in
|
|
24
|
+
* `backend/src/yagniCode/crashReports.ts`. Spec:
|
|
25
|
+
* docs/superpowers/specs/2026-08-08-crash-reporting-design.md
|
|
26
|
+
*/
|
|
27
|
+
import { spawn } from "node:child_process";
|
|
28
|
+
import { scrubSecrets } from "./pipeline/scrubSecrets.js";
|
|
29
|
+
import { isDesktopSurface } from "./surface.js";
|
|
30
|
+
const defaultSpawn = (command, args, options) => spawn(command, args, options);
|
|
31
|
+
export const CRASH_REPORT_DISABLE_ENV = "YAGNI_DISABLE_CRASH_REPORTS";
|
|
32
|
+
export const CRASH_REPORT_TIMEOUT_MS = 1_500;
|
|
33
|
+
export const MAX_CRASH_MESSAGE = 512;
|
|
34
|
+
export const MAX_CRASH_ERROR_CLASS = 128;
|
|
35
|
+
export const MAX_CRASH_STACK = 8_000;
|
|
36
|
+
export const MAX_CRASH_STACK_FRAMES = 40;
|
|
37
|
+
export const MAX_CRASH_PAYLOAD_BYTES = 16_384;
|
|
38
|
+
/** Same truthiness rule as the CLI's YAGNI_DISABLE_* family. */
|
|
39
|
+
export function crashReportsDisabled(env = process.env) {
|
|
40
|
+
const value = env[CRASH_REPORT_DISABLE_ENV];
|
|
41
|
+
return value !== undefined && value !== "" && value !== "0";
|
|
42
|
+
}
|
|
43
|
+
const HOME_DIR_RE = /(?:\/(?:Users|home)\/|[A-Za-z]:\\Users\\)[^\s/\\]+/g;
|
|
44
|
+
// Spaces deliberately allowed inside the token (real directories contain
|
|
45
|
+
// them); prose after a path may fold into the kept basename — over-redacts
|
|
46
|
+
// rather than under-redacts. Keep in sync with the CLI copy.
|
|
47
|
+
const PATH_TOKEN_RE = /(?<![\w.~/\\])(?:~\/|\/|[A-Za-z]:\\)[^\n():'",]+/g;
|
|
48
|
+
const MIN_ENV_VALUE_LENGTH = 8;
|
|
49
|
+
function isPathLikeValue(value) {
|
|
50
|
+
return value.startsWith("/") || value.startsWith("~") || /^[A-Za-z]:\\/.test(value);
|
|
51
|
+
}
|
|
52
|
+
function collapsePathToken(token) {
|
|
53
|
+
const nm = token.lastIndexOf("node_modules/");
|
|
54
|
+
if (nm >= 0)
|
|
55
|
+
return token.slice(nm);
|
|
56
|
+
const cut = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
|
|
57
|
+
const base = cut >= 0 ? token.slice(cut + 1) : token;
|
|
58
|
+
return base.length > 0 ? base : token;
|
|
59
|
+
}
|
|
60
|
+
/** Env values → [ENV:NAME], scrubSecrets patterns, ~-collapse, repo-relative, basename. */
|
|
61
|
+
export function sanitizeCrashText(text, opts = {}) {
|
|
62
|
+
let out = text;
|
|
63
|
+
const env = opts.env ?? process.env;
|
|
64
|
+
const entries = Object.entries(env)
|
|
65
|
+
.filter((entry) => typeof entry[1] === "string" &&
|
|
66
|
+
entry[1].length >= MIN_ENV_VALUE_LENGTH &&
|
|
67
|
+
!isPathLikeValue(entry[1]))
|
|
68
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
69
|
+
for (const [name, value] of entries) {
|
|
70
|
+
if (out.includes(value))
|
|
71
|
+
out = out.split(value).join(`[ENV:${name}]`);
|
|
72
|
+
}
|
|
73
|
+
out = scrubSecrets(out);
|
|
74
|
+
out = out.replace(HOME_DIR_RE, "~");
|
|
75
|
+
if (opts.repoRoot) {
|
|
76
|
+
const root = opts.repoRoot.replace(HOME_DIR_RE, "~").replace(/\/+$/, "");
|
|
77
|
+
if (root.length > 1)
|
|
78
|
+
out = out.split(`${root}/`).join("");
|
|
79
|
+
}
|
|
80
|
+
out = out.replace(PATH_TOKEN_RE, collapsePathToken);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/** Reads ONLY name/message/stack; custom error fields are never touched. */
|
|
84
|
+
export function sanitizeCrashError(err, opts = {}) {
|
|
85
|
+
let errorClass = "Error";
|
|
86
|
+
let message;
|
|
87
|
+
let stack;
|
|
88
|
+
if (err instanceof Error) {
|
|
89
|
+
errorClass = err.name || "Error";
|
|
90
|
+
message = err.message;
|
|
91
|
+
stack = typeof err.stack === "string" ? err.stack : undefined;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
try {
|
|
95
|
+
message = String(err);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
message = "unstringifiable thrown value";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const cappedStack = stack !== undefined
|
|
102
|
+
? sanitizeCrashText(stack, opts)
|
|
103
|
+
.split("\n")
|
|
104
|
+
.slice(0, MAX_CRASH_STACK_FRAMES)
|
|
105
|
+
.join("\n")
|
|
106
|
+
.slice(0, MAX_CRASH_STACK)
|
|
107
|
+
: undefined;
|
|
108
|
+
return {
|
|
109
|
+
errorClass: sanitizeCrashText(errorClass, opts).slice(0, MAX_CRASH_ERROR_CLASS),
|
|
110
|
+
message: sanitizeCrashText(message, opts).slice(0, MAX_CRASH_MESSAGE),
|
|
111
|
+
...(cappedStack !== undefined ? { stack: cappedStack } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build the fail-soft reporter. The extension runs inside pi's process, so
|
|
116
|
+
* the client label follows the surface: `desktop` under the desktop shell
|
|
117
|
+
* (YAGNI_SURFACE=desktop), `cli` in the terminal. Version comes from
|
|
118
|
+
* `YAGNI_CODE_VERSION` (forwarded by the launcher; the bundled extension has
|
|
119
|
+
* no package.json on disk). Resolves on every outcome; never throws.
|
|
120
|
+
*/
|
|
121
|
+
export function makeCrashReporter(opts) {
|
|
122
|
+
return async (error, context, repoRoot) => {
|
|
123
|
+
try {
|
|
124
|
+
const env = opts.env ?? process.env;
|
|
125
|
+
if (crashReportsDisabled(env))
|
|
126
|
+
return;
|
|
127
|
+
const token = opts.getToken();
|
|
128
|
+
if (!token)
|
|
129
|
+
return;
|
|
130
|
+
const sanitized = sanitizeCrashError(error, { env, repoRoot });
|
|
131
|
+
const payload = {
|
|
132
|
+
client: isDesktopSurface() ? "desktop" : "cli",
|
|
133
|
+
clientVersion: env.YAGNI_CODE_VERSION?.trim() || "unknown",
|
|
134
|
+
platform: `${process.platform} ${process.arch}`,
|
|
135
|
+
...sanitized,
|
|
136
|
+
...(context !== undefined ? { context } : {}),
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
};
|
|
139
|
+
if (JSON.stringify(payload).length > MAX_CRASH_PAYLOAD_BYTES) {
|
|
140
|
+
delete payload.stack;
|
|
141
|
+
}
|
|
142
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? CRASH_REPORT_TIMEOUT_MS);
|
|
145
|
+
timer.unref?.();
|
|
146
|
+
try {
|
|
147
|
+
await fetchImpl(`${opts.baseUrl.replace(/\/$/, "")}/api/yagni-code/crash`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: {
|
|
150
|
+
"content-type": "application/json",
|
|
151
|
+
authorization: `Bearer ${token}`,
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify(payload),
|
|
154
|
+
signal: controller.signal,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// fail-soft by contract
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* One-shot sender source for the detached delivery child. A fatal uncaught
|
|
168
|
+
* exception kills pi's process before any in-process fetch could connect, so
|
|
169
|
+
* the monitor path hands the (already sanitized) payload to a detached
|
|
170
|
+
* `node -e` child that survives the parent's death, POSTs once with a hard
|
|
171
|
+
* timeout, and exits. No retries, nothing written to disk. The payload, URL
|
|
172
|
+
* and token travel via the child's env — the same channel the launcher
|
|
173
|
+
* already uses to hand pi its YAGNI_TOKEN.
|
|
174
|
+
*/
|
|
175
|
+
const DETACHED_SENDER_SRC = [
|
|
176
|
+
"const done = () => process.exit(0);",
|
|
177
|
+
"setTimeout(done, 5000).unref?.();",
|
|
178
|
+
"fetch(process.env.YAGNI_CRASH_URL, {",
|
|
179
|
+
" method: 'POST',",
|
|
180
|
+
" headers: {",
|
|
181
|
+
" 'content-type': 'application/json',",
|
|
182
|
+
" authorization: 'Bearer ' + process.env.YAGNI_CRASH_TOKEN,",
|
|
183
|
+
" },",
|
|
184
|
+
" body: process.env.YAGNI_CRASH_PAYLOAD,",
|
|
185
|
+
" signal: AbortSignal.timeout(4000),",
|
|
186
|
+
"}).catch(() => {}).finally(done);",
|
|
187
|
+
].join("\n");
|
|
188
|
+
/**
|
|
189
|
+
* Deliver a crash report from a process that is about to die: build the
|
|
190
|
+
* sanitized payload in-process (cheap, synchronous), then spawn a detached
|
|
191
|
+
* one-shot sender that outlives the crash. Never throws.
|
|
192
|
+
*/
|
|
193
|
+
export function reportFatalCrash(error, opts, context) {
|
|
194
|
+
try {
|
|
195
|
+
const env = opts.env ?? process.env;
|
|
196
|
+
if (crashReportsDisabled(env))
|
|
197
|
+
return;
|
|
198
|
+
const token = opts.getToken();
|
|
199
|
+
if (!token)
|
|
200
|
+
return;
|
|
201
|
+
const sanitized = sanitizeCrashError(error, { env });
|
|
202
|
+
const payload = {
|
|
203
|
+
client: isDesktopSurface() ? "desktop" : "cli",
|
|
204
|
+
clientVersion: env.YAGNI_CODE_VERSION?.trim() || "unknown",
|
|
205
|
+
platform: `${process.platform} ${process.arch}`,
|
|
206
|
+
...sanitized,
|
|
207
|
+
...(context !== undefined ? { context } : {}),
|
|
208
|
+
timestamp: new Date().toISOString(),
|
|
209
|
+
};
|
|
210
|
+
if (JSON.stringify(payload).length > MAX_CRASH_PAYLOAD_BYTES) {
|
|
211
|
+
delete payload.stack;
|
|
212
|
+
}
|
|
213
|
+
const spawnImpl = opts.spawnImpl ?? defaultSpawn;
|
|
214
|
+
const child = spawnImpl(process.execPath, ["-e", DETACHED_SENDER_SRC], {
|
|
215
|
+
detached: true,
|
|
216
|
+
stdio: "ignore",
|
|
217
|
+
env: {
|
|
218
|
+
// Minimal env: the sender needs nothing from the parent beyond these.
|
|
219
|
+
YAGNI_CRASH_URL: `${opts.baseUrl.replace(/\/$/, "")}/api/yagni-code/crash`,
|
|
220
|
+
YAGNI_CRASH_TOKEN: token,
|
|
221
|
+
YAGNI_CRASH_PAYLOAD: JSON.stringify(payload),
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
child.unref();
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// a crash reporter must never add its own crash
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Observe (never alter) a fatal crash in pi's process. Uses
|
|
232
|
+
* `uncaughtExceptionMonitor`, which fires before the process dies without
|
|
233
|
+
* installing a handler that would change node's default crash behavior —
|
|
234
|
+
* YAGNI does not own pi's process, so crash semantics must stay untouched.
|
|
235
|
+
*/
|
|
236
|
+
export function installUncaughtExceptionMonitor(opts, proc = process) {
|
|
237
|
+
proc.on("uncaughtExceptionMonitor", (err) => {
|
|
238
|
+
reportFatalCrash(err, opts, "uncaught-exception");
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=crashReport.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
|
|
@@ -0,0 +1,200 @@
|
|
|
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 { randomUUID } from "node:crypto";
|
|
21
|
+
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
22
|
+
import { sendOrSpool } from "./spool.js";
|
|
23
|
+
/**
|
|
24
|
+
* Bank a `cli_correction` decision durably. Every logical write carries a
|
|
25
|
+
* generated idempotencyKey; the backend dedups on it, so a spool replay or a
|
|
26
|
+
* 401 retry can never bank the same decision twice. Transport failures and 5xx
|
|
27
|
+
* are spooled for replay instead of lost (R4 write half). Never throws.
|
|
28
|
+
*/
|
|
29
|
+
export function bankDecision(opts, params, signal) {
|
|
30
|
+
const idempotencyKey = (opts.makeIdempotencyKey ?? randomUUID)();
|
|
31
|
+
return sendOrSpool(opts, "record_decision", "/api/yagni-code/decisions", {
|
|
32
|
+
question: params.question,
|
|
33
|
+
decision: params.decision,
|
|
34
|
+
rationale: params.rationale,
|
|
35
|
+
repo: params.repo,
|
|
36
|
+
source: "cli_correction",
|
|
37
|
+
}, idempotencyKey, signal);
|
|
38
|
+
}
|
|
39
|
+
export const DEFAULT_LIST_LIMIT = 20;
|
|
40
|
+
/** GET recent decisions (any read scope). Throws a friendly error on non-2xx. */
|
|
41
|
+
export async function fetchDecisions(opts, list = {}, signal) {
|
|
42
|
+
const limit = list.limit ?? DEFAULT_LIST_LIMIT;
|
|
43
|
+
const query = new URLSearchParams({ limit: String(limit) });
|
|
44
|
+
if (list.repo)
|
|
45
|
+
query.set("repo", list.repo);
|
|
46
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/decisions?${query.toString()}`, { method: "GET", headers: { authorization: `Bearer ${opts.getToken() ?? ""}` } }, { fetchImpl: opts.fetchImpl, signal });
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
throw new Error(await friendlyFetchError("list decisions", res));
|
|
49
|
+
const data = (await res.json());
|
|
50
|
+
return Array.isArray(data.decisions) ? data.decisions : [];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* PATCH one decision to `superseded` (scope yagni_code:write). Idempotent flip,
|
|
54
|
+
* so a single attempt is enough — no client retry (invariant §0.8). Throws a
|
|
55
|
+
* friendly error on non-2xx.
|
|
56
|
+
*/
|
|
57
|
+
export async function supersedeDecision(opts, id, supersededBy, signal) {
|
|
58
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/decisions/${encodeURIComponent(id)}`, {
|
|
59
|
+
method: "PATCH",
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": "application/json",
|
|
62
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ status: "superseded", ...(supersededBy ? { supersededBy } : {}) }),
|
|
65
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
66
|
+
if (!res.ok)
|
|
67
|
+
throw new Error(await friendlyFetchError("supersede decision", res));
|
|
68
|
+
}
|
|
69
|
+
/** Pure: parse `<question> => <decision>`; null when either half is missing. */
|
|
70
|
+
export function parseDecideArgs(raw) {
|
|
71
|
+
const idx = raw.indexOf("=>");
|
|
72
|
+
if (idx < 0)
|
|
73
|
+
return null;
|
|
74
|
+
const question = raw.slice(0, idx).trim();
|
|
75
|
+
const decision = raw.slice(idx + 2).trim();
|
|
76
|
+
if (!question || !decision)
|
|
77
|
+
return null;
|
|
78
|
+
return { question, decision };
|
|
79
|
+
}
|
|
80
|
+
/** Pure: a short id prefix for compact display. */
|
|
81
|
+
export function shortId(id) {
|
|
82
|
+
return id.slice(0, 8);
|
|
83
|
+
}
|
|
84
|
+
/** Pure: render the compact, id-prefixed decisions list (no em-dashes). */
|
|
85
|
+
export function formatDecisionsList(items) {
|
|
86
|
+
if (items.length === 0) {
|
|
87
|
+
return "No decisions recorded yet. Use /decide <question> => <decision> to bank one.";
|
|
88
|
+
}
|
|
89
|
+
const header = `Recorded decisions (${items.length}):`;
|
|
90
|
+
const lines = items.map((d) => {
|
|
91
|
+
const flag = d.status === "active" ? "" : ` [${d.status}]`;
|
|
92
|
+
const cited = d.citedCount > 0 ? ` (cited ${d.citedCount})` : "";
|
|
93
|
+
return `${shortId(d.id)}${flag} ${d.question} => ${d.decision}${cited}`;
|
|
94
|
+
});
|
|
95
|
+
return [header, ...lines].join("\n");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a displayed short-id prefix (or a full id) to the full decision id by
|
|
99
|
+
* matching against the current list. Returns the full id, the raw arg when no
|
|
100
|
+
* match is found (let the backend answer an honest 404), or "ambiguous" when
|
|
101
|
+
* more than one decision shares the prefix. Fail-soft: a list error falls back
|
|
102
|
+
* to the raw arg.
|
|
103
|
+
*/
|
|
104
|
+
async function resolveDecisionId(opts, arg, signal) {
|
|
105
|
+
let items = [];
|
|
106
|
+
try {
|
|
107
|
+
items = await fetchDecisions(opts, {}, signal);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return arg; // cannot resolve → let the PATCH itself surface the outcome
|
|
111
|
+
}
|
|
112
|
+
const matches = items.filter((d) => d.id === arg || d.id.startsWith(arg));
|
|
113
|
+
if (matches.length === 1)
|
|
114
|
+
return matches[0].id;
|
|
115
|
+
if (matches.length > 1)
|
|
116
|
+
return "ambiguous";
|
|
117
|
+
return arg;
|
|
118
|
+
}
|
|
119
|
+
/** A friendly one-liner for a bank outcome, or null when nothing to say. */
|
|
120
|
+
function bankOutcomeNotice(outcome) {
|
|
121
|
+
if (outcome.kind === "ok")
|
|
122
|
+
return { message: "Recorded the decision in YAGNI.", type: "info" };
|
|
123
|
+
if (outcome.kind === "spooled") {
|
|
124
|
+
return {
|
|
125
|
+
message: `Could not reach YAGNI (${outcome.reason}). Saved the decision locally; it will sync automatically.`,
|
|
126
|
+
type: "warning",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { message: outcome.message, type: "error" };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Wire the `/decide` and `/decisions` commands. Both are fail-soft: any
|
|
133
|
+
* transport error surfaces as an honest notice, never a thrown command.
|
|
134
|
+
*/
|
|
135
|
+
export function registerDecisionCommands(pi, opts) {
|
|
136
|
+
pi.registerCommand("decide", {
|
|
137
|
+
description: "Record a product-intent decision directly: /decide <question> => <decision>. YAGNI answers the same question next time.",
|
|
138
|
+
handler: async (args, ctx) => {
|
|
139
|
+
const notify = (m, t) => {
|
|
140
|
+
if (ctx.hasUI)
|
|
141
|
+
ctx.ui.notify(m, t);
|
|
142
|
+
};
|
|
143
|
+
const parsed = parseDecideArgs(args);
|
|
144
|
+
if (!parsed) {
|
|
145
|
+
notify("Usage: /decide <question> => <decision>", "warning");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const outcome = await bankDecision(opts, { question: parsed.question, decision: parsed.decision }, ctx.signal);
|
|
150
|
+
const notice = bankOutcomeNotice(outcome);
|
|
151
|
+
notify(notice.message, notice.type);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
notify(`Could not record the decision: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
pi.registerCommand("decisions", {
|
|
159
|
+
description: "List recent recorded decisions. /decisions supersede <id> marks one superseded.",
|
|
160
|
+
handler: async (args, ctx) => {
|
|
161
|
+
const notify = (m, t) => {
|
|
162
|
+
if (ctx.hasUI)
|
|
163
|
+
ctx.ui.notify(m, t);
|
|
164
|
+
};
|
|
165
|
+
const trimmed = args.trim();
|
|
166
|
+
if (trimmed.toLowerCase().startsWith("supersede")) {
|
|
167
|
+
const arg = trimmed.slice("supersede".length).trim();
|
|
168
|
+
if (!arg) {
|
|
169
|
+
notify("Usage: /decisions supersede <id>", "warning");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
// The list shows short id prefixes, so resolve the argument against
|
|
174
|
+
// the current list to the full UUID the backend PATCH requires. A
|
|
175
|
+
// failed lookup falls through with the raw arg (the backend then
|
|
176
|
+
// answers an honest 404).
|
|
177
|
+
const id = await resolveDecisionId(opts, arg, ctx.signal);
|
|
178
|
+
if (id === "ambiguous") {
|
|
179
|
+
notify(`Ambiguous decision id "${arg}"; use more characters.`, "warning");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await supersedeDecision(opts, id, undefined, ctx.signal);
|
|
183
|
+
notify(`Superseded decision ${shortId(id)}.`, "info");
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
notify(`Could not supersede decision: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const items = await fetchDecisions(opts, {}, ctx.signal);
|
|
192
|
+
await pi.sendUserMessage(formatDecisionsList(items));
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
notify(`Could not list decisions: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=decisions.js.map
|