@primitive.ai/prim 0.1.0-alpha.8 → 0.1.0-alpha.81

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.
Files changed (45) hide show
  1. package/README.md +295 -37
  2. package/SKILL.md +269 -0
  3. package/dist/chunk-3EEFL6NJ.js +529 -0
  4. package/dist/chunk-4CZTNXIS.js +505 -0
  5. package/dist/chunk-4GLVJLEQ.js +55 -0
  6. package/dist/chunk-BQUJ4AFJ.js +771 -0
  7. package/dist/chunk-CUE2LKLR.js +155 -0
  8. package/dist/chunk-EZXEWFDG.js +210 -0
  9. package/dist/chunk-FI3LIRCB.js +262 -0
  10. package/dist/chunk-HWSS4BV5.js +1400 -0
  11. package/dist/chunk-LEMX3IOG.js +79 -0
  12. package/dist/chunk-LMRYZL3D.js +209 -0
  13. package/dist/chunk-OUXH3UEP.js +247 -0
  14. package/dist/chunk-PMRDE2DA.js +310 -0
  15. package/dist/chunk-RO3F6JYV.js +126 -0
  16. package/dist/chunk-TJTVHDJ5.js +321 -0
  17. package/dist/chunk-WB7AUZ4C.js +52 -0
  18. package/dist/chunk-XGADQPCI.js +26 -0
  19. package/dist/chunk-YWO7O6QG.js +327 -0
  20. package/dist/chunk-Z7YPFWAZ.js +917 -0
  21. package/dist/chunk-ZOSNEO5N.js +77 -0
  22. package/dist/daemon/server.js +15329 -0
  23. package/dist/hook-runtime/daemon/server.js +15329 -0
  24. package/dist/hook-runtime/hooks/post-commit.js +1128 -0
  25. package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
  26. package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
  27. package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
  28. package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
  29. package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
  30. package/dist/hook-runtime/hooks/session-end.js +495 -0
  31. package/dist/hook-runtime/hooks/session-start.js +11313 -0
  32. package/dist/hook-runtime/index.js +44696 -0
  33. package/dist/hook-runtime/statusline-main.js +765 -0
  34. package/dist/hooks/post-commit.js +179 -0
  35. package/dist/hooks/post-rewrite.js +197 -0
  36. package/dist/hooks/post-tool-use.js +250 -0
  37. package/dist/hooks/pre-commit.js +21 -74
  38. package/dist/hooks/pre-tool-use.js +348 -0
  39. package/dist/hooks/prim-hook.js +195 -0
  40. package/dist/hooks/session-end.js +70 -0
  41. package/dist/hooks/session-start.js +299 -0
  42. package/dist/index.js +20981 -519
  43. package/dist/statusline-main.js +765 -0
  44. package/package.json +35 -13
  45. package/dist/chunk-3APLWTLB.js +0 -165
@@ -0,0 +1,310 @@
1
+ import {
2
+ stripControlChars
3
+ } from "./chunk-Z7YPFWAZ.js";
4
+
5
+ // src/lib/bin-path.ts
6
+ import { existsSync, readFileSync } from "fs";
7
+ import { dirname, isAbsolute, join } from "path";
8
+ import { fileURLToPath } from "url";
9
+
10
+ // src/lib/semver.ts
11
+ function parseSemver(value) {
12
+ if (typeof value !== "string" || value.length > 256) return;
13
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(
14
+ value
15
+ );
16
+ if (!match) return;
17
+ const prerelease = match[4]?.split(".") ?? [];
18
+ if (prerelease.some((part) => /^\d+$/u.test(part) && part.length > 1 && part.startsWith("0"))) {
19
+ return;
20
+ }
21
+ return { core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])], prerelease };
22
+ }
23
+ function compareSemver(left, right) {
24
+ const a = parseSemver(left);
25
+ const b = parseSemver(right);
26
+ if (!a || !b) return;
27
+ for (let i = 0; i < a.core.length; i += 1) {
28
+ if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
29
+ }
30
+ if (a.prerelease.length === 0 || b.prerelease.length === 0) {
31
+ return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
32
+ }
33
+ for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i += 1) {
34
+ const x = a.prerelease[i];
35
+ const y = b.prerelease[i];
36
+ if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
37
+ if (x === y) continue;
38
+ const xNumeric = /^\d+$/u.test(x);
39
+ const yNumeric = /^\d+$/u.test(y);
40
+ if (xNumeric && yNumeric) return BigInt(x) < BigInt(y) ? -1 : 1;
41
+ if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
42
+ return x < y ? -1 : 1;
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ // src/lib/bin-path.ts
48
+ var PKG_NAME = "@primitive.ai/prim";
49
+ var ROOT_WALK_LIMIT = 6;
50
+ var STABLE_HOOK_LAUNCHER_NAME = "prim-hook-launcher-v1";
51
+ var resolvedRoot;
52
+ function locateRoot() {
53
+ if (resolvedRoot !== void 0) {
54
+ return resolvedRoot;
55
+ }
56
+ let dir = dirname(fileURLToPath(import.meta.url));
57
+ for (let depth = 0; depth < ROOT_WALK_LIMIT; depth++) {
58
+ const manifestPath = join(dir, "package.json");
59
+ if (existsSync(manifestPath)) {
60
+ try {
61
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
62
+ if (manifest.name === PKG_NAME && manifest.bin) {
63
+ resolvedRoot = { dir, version: manifest.version, bin: manifest.bin };
64
+ return resolvedRoot;
65
+ }
66
+ } catch {
67
+ }
68
+ }
69
+ const parent = dirname(dir);
70
+ if (parent === dir) {
71
+ break;
72
+ }
73
+ dir = parent;
74
+ }
75
+ resolvedRoot = null;
76
+ return resolvedRoot;
77
+ }
78
+ function binFile(bin) {
79
+ const root = locateRoot();
80
+ const rel = root?.bin[bin];
81
+ if (!root || !rel) {
82
+ return null;
83
+ }
84
+ return isAbsolute(rel) ? rel : join(root.dir, rel);
85
+ }
86
+ function packageVersion() {
87
+ return locateRoot()?.version ?? null;
88
+ }
89
+ function packageRoot() {
90
+ return locateRoot()?.dir ?? null;
91
+ }
92
+ function shellQuote(value) {
93
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
94
+ }
95
+ function shellWord(value) {
96
+ return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(value) ? value : shellQuote(value);
97
+ }
98
+ function pinnedNpxArgs(bin, args = [], options = {}) {
99
+ const version = packageVersion();
100
+ if (!version) throw new Error("cannot determine Primitive package version");
101
+ return [
102
+ "--yes",
103
+ "--ignore-scripts",
104
+ ...options.preferOnline ? ["--prefer-online"] : [],
105
+ "-p",
106
+ `${PKG_NAME}@${version}`,
107
+ bin,
108
+ ...args
109
+ ];
110
+ }
111
+ function pinnedNpxCommand(bin, args = [], options = {}) {
112
+ return ["npx", ...pinnedNpxArgs(bin, args, options)].map(shellWord).join(" ");
113
+ }
114
+ function pinnedHookCommand(bin, args = "") {
115
+ const suffix = args ? ` ${args}` : "";
116
+ const fallback = `${pinnedNpxCommand(bin)}${suffix}`;
117
+ const file = binFile(bin);
118
+ if (!file) return fallback;
119
+ return `if [ -x ${shellQuote(process.execPath)} ] && [ -f ${shellQuote(file)} ]; then ${shellQuote(process.execPath)} ${shellQuote(file)}${suffix}; else ${fallback}; fi`;
120
+ }
121
+ var STABLE_HOOK_ARGUMENTS_RE = /^[-A-Za-z0-9_ ]*$/u;
122
+ var STABLE_HOOK_BIN_RE = /^[A-Za-z0-9][A-Za-z0-9-]*$/u;
123
+ function stableHookCommand(bin, args = "") {
124
+ if (!STABLE_HOOK_BIN_RE.test(bin) || !STABLE_HOOK_ARGUMENTS_RE.test(args)) {
125
+ throw new Error("invalid stable hook command");
126
+ }
127
+ const resolver = [
128
+ // #242 and earlier identify managed commands by this package marker. Keep
129
+ // it as inert data during the rolling window so an older uninstall or
130
+ // reinstall can remove the stable entry without invoking npm or PATH.
131
+ `prim_legacy_reader='-p ${PKG_NAME}@stable prim-shim.sh ${bin} '; `,
132
+ 'prim_absolute() { case "$1" in /*) ;; *) return 1 ;; esac; case "$1" in [[:space:]]*|*[[:space:]]|*//*|*/./*|*/../*|*/.|*/..|?*/) return 1 ;; esac; }; ',
133
+ 'prim_config=${PRIM_CONFIG_DIR:-}; if ! prim_absolute "$prim_config"; then prim_config=${XDG_CONFIG_HOME:-}; if prim_absolute "$prim_config"; then ',
134
+ 'case "$prim_config" in /) prim_config=/prim ;; *) prim_config="$prim_config/prim" ;; esac; ',
135
+ 'else prim_home=${HOME:-}; prim_absolute "$prim_home" || exit 78; case "$prim_home" in /) prim_config=/.config/prim ;; *) prim_config="$prim_home/.config/prim" ;; esac; ',
136
+ "fi; fi; ",
137
+ `case "$prim_config" in /) prim_launcher=/${STABLE_HOOK_LAUNCHER_NAME} ;; *) prim_launcher="$prim_config/${STABLE_HOOK_LAUNCHER_NAME}" ;; esac; `,
138
+ 'exec "$prim_launcher" "$@"'
139
+ ].join("");
140
+ const suffix = args ? ` ${args}` : "";
141
+ return `/bin/sh -c ${shellQuote(resolver)} ${STABLE_HOOK_LAUNCHER_NAME} ${bin}${suffix}`;
142
+ }
143
+ function detachedHookShimCommand(bin, args = "") {
144
+ return `payload=$(cat); { trap '' HUP; printf '%s' "$payload" | { ${stableHookCommand(bin, args)}; }; } </dev/null >/dev/null 2>&1 &`;
145
+ }
146
+ function commandMatchesBin(command, bin) {
147
+ if (!command) {
148
+ return false;
149
+ }
150
+ const c = command.trim();
151
+ if (c === bin || c.startsWith(`${bin} `)) {
152
+ return true;
153
+ }
154
+ const exactBin = new RegExp(`(?:^|\\s)${bin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|;|$)`);
155
+ return c.includes(`command -v ${bin} `) || c.includes(`-p ${PKG_NAME}@`) && exactBin.test(c) || c.includes(STABLE_HOOK_LAUNCHER_NAME) && exactBin.test(c);
156
+ }
157
+ function exactPinnedNpxVersion(command, bin) {
158
+ const escapedBin = bin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
159
+ const shellQuoted = `'(?:[^']|'"'"')*'`;
160
+ const safeArgs = "(?: [A-Za-z0-9_@%+=:,./-]+)*";
161
+ const direct = new RegExp(
162
+ `^npx --yes --ignore-scripts -p @primitive\\.ai/prim@(?<version>[0-9A-Za-z.+-]+) ${escapedBin}${safeArgs}$`,
163
+ "u"
164
+ ).exec(command);
165
+ const wrapped = new RegExp(
166
+ `^if \\[ -x (?<node>${shellQuoted}) \\] && \\[ -f (?<entry>${shellQuoted}) \\]; then \\k<node> \\k<entry>(?<args>${safeArgs}); else npx --yes --ignore-scripts -p @primitive\\.ai/prim@(?<version>[0-9A-Za-z.+-]+) ${escapedBin}\\k<args>; fi$`,
167
+ "u"
168
+ ).exec(command);
169
+ const version = direct?.groups?.version ?? wrapped?.groups?.version;
170
+ return version && compareSemver(version, version) !== void 0 ? version : void 0;
171
+ }
172
+ function hookCommandResolution(command, bin) {
173
+ const trimmed = command?.trim();
174
+ if (!trimmed || !commandMatchesBin(trimmed, bin)) return void 0;
175
+ if (trimmed.includes(STABLE_HOOK_LAUNCHER_NAME)) return { kind: "stable_launcher" };
176
+ const version = exactPinnedNpxVersion(trimmed, bin);
177
+ return version ? { kind: "exact_npx_fallback", version } : { kind: "legacy_path" };
178
+ }
179
+ function hookCommandResolutions(commands, bins) {
180
+ const resolutions = [];
181
+ for (const command of commands) {
182
+ for (const bin of bins) {
183
+ const resolution = hookCommandResolution(command, bin);
184
+ if (resolution) resolutions.push(resolution);
185
+ }
186
+ }
187
+ return resolutions;
188
+ }
189
+
190
+ // src/lib/presence.ts
191
+ var DECISION_ORIGIN = "https://app.getprimitive.ai";
192
+ function validDecisionUrl(value) {
193
+ if (typeof value !== "string" || stripControlChars(value) !== value) {
194
+ return void 0;
195
+ }
196
+ try {
197
+ const url = new URL(value);
198
+ if (url.origin !== DECISION_ORIGIN || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || !/^\/decisions\/[^/]+$/u.test(url.pathname)) {
199
+ return void 0;
200
+ }
201
+ return url.href;
202
+ } catch {
203
+ return void 0;
204
+ }
205
+ }
206
+ function decisionLink(label, decisionUrl, plain) {
207
+ const url = validDecisionUrl(decisionUrl);
208
+ if (plain || !url) {
209
+ return label;
210
+ }
211
+ return `\x1B]8;;${url}\x07\x1B[34;4m${label}\x1B[0m\x1B]8;;\x07`;
212
+ }
213
+ function formatLabeled(labels, cap) {
214
+ if (labels === void 0) {
215
+ return "\u2014";
216
+ }
217
+ if (labels.length === 0) {
218
+ return "just you";
219
+ }
220
+ if (labels.length <= cap) {
221
+ return labels.join(", ");
222
+ }
223
+ return `${labels.slice(0, cap).join(", ")} +${String(labels.length - cap)}`;
224
+ }
225
+ function formatTeammates(names, cap) {
226
+ return formatLabeled(names?.map(stripControlChars), cap);
227
+ }
228
+ function formatTeammatesWithArea(teammates, cap, plainLinks = false) {
229
+ return formatLabeled(
230
+ teammates?.map((t) => {
231
+ const name = stripControlChars(t.name);
232
+ const area = stripControlChars(t.area ?? "").trim();
233
+ const label = area ? `${name} - ${area}` : name;
234
+ return decisionLink(label, t.decisionUrl, plainLinks);
235
+ }),
236
+ cap
237
+ );
238
+ }
239
+
240
+ // src/lib/statusline-render.ts
241
+ function repositoryBindingDiagnosticLabel(state) {
242
+ if (state === "unbound") return "GitHub repo connection: required (run `prim github connect`)";
243
+ if (state === "invalid") return "GitHub repo connection: invalid (run `prim doctor`)";
244
+ return void 0;
245
+ }
246
+ var STATUSLINE_NAME_CAP = 3;
247
+ function formatStatusline(version, snapshot, resolveIngestionStatus, options = {}) {
248
+ const ingestionSuffix = (status) => status === void 0 ? "" : ` \xB7 Decision ingestion ${status}`;
249
+ const ingestionStatus = snapshot === null || snapshot.healthy === false ? options.includeIngestionWhenUnavailable ? resolveIngestionStatus() : void 0 : resolveIngestionStatus();
250
+ if (snapshot?.healthy === false && snapshot.needsReauth) {
251
+ return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`${ingestionSuffix(ingestionStatus)})`;
252
+ }
253
+ const repositoryLabel = repositoryBindingDiagnosticLabel(
254
+ snapshot !== null && snapshot.healthy !== false || options.includeIngestionWhenUnavailable === true ? options.resolveRepositoryBindingState?.() : void 0
255
+ );
256
+ const repositorySuffix = repositoryLabel ? ` \xB7 ${repositoryLabel}` : "";
257
+ if (!snapshot) {
258
+ return `primitive ${version} (daemon: down${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
259
+ }
260
+ if (snapshot.healthy === false) {
261
+ if (snapshot.ingestion?.healthy === false) {
262
+ const pending = snapshot.ingestion.pendingCount;
263
+ const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
264
+ return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
265
+ }
266
+ if (snapshot.heartbeat?.healthy === false) {
267
+ return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
268
+ }
269
+ return `primitive ${version} (daemon: starting${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
270
+ }
271
+ if (snapshot.envMismatch) {
272
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: other env)`;
273
+ }
274
+ if (snapshot.principalMismatch) {
275
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: other account)`;
276
+ }
277
+ if (snapshot.presenceStale) {
278
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: stale)`;
279
+ }
280
+ let team;
281
+ if (snapshot.onlineTeammates !== void 0) {
282
+ team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP, options.plainLinks === true)}`;
283
+ } else if (snapshot.onlineNames !== void 0) {
284
+ team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
285
+ } else if (typeof snapshot.onlineCount === "number") {
286
+ team = `team: ${String(snapshot.onlineCount)} online`;
287
+ } else {
288
+ team = "team: \u2014";
289
+ }
290
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 ${team})`;
291
+ }
292
+
293
+ export {
294
+ compareSemver,
295
+ STABLE_HOOK_LAUNCHER_NAME,
296
+ binFile,
297
+ packageVersion,
298
+ packageRoot,
299
+ pinnedNpxArgs,
300
+ pinnedNpxCommand,
301
+ pinnedHookCommand,
302
+ stableHookCommand,
303
+ detachedHookShimCommand,
304
+ commandMatchesBin,
305
+ hookCommandResolution,
306
+ hookCommandResolutions,
307
+ formatTeammates,
308
+ repositoryBindingDiagnosticLabel,
309
+ formatStatusline
310
+ };
@@ -0,0 +1,126 @@
1
+ import {
2
+ getClient,
3
+ getSiteUrl,
4
+ terminalSafeLine
5
+ } from "./chunk-Z7YPFWAZ.js";
6
+ import {
7
+ daemonRequest
8
+ } from "./chunk-CUE2LKLR.js";
9
+
10
+ // src/daemon/proxy.ts
11
+ var DAEMON_HTTP_TIMEOUT_MS = 1e4;
12
+ var DAEMON_PROBE_TIMEOUT_MS = 250;
13
+ async function daemonOrDirect(method, params, direct) {
14
+ const fromDaemon = await daemonRequest(
15
+ method,
16
+ { ...params, callerEnv: getSiteUrl() },
17
+ { timeoutMs: DAEMON_PROBE_TIMEOUT_MS }
18
+ );
19
+ if (fromDaemon !== null) {
20
+ return fromDaemon;
21
+ }
22
+ return await direct();
23
+ }
24
+ async function daemonOrDirectGet(method, path, client, timeoutMs = DAEMON_HTTP_TIMEOUT_MS) {
25
+ return await daemonOrDirect(
26
+ method,
27
+ { path },
28
+ async () => await client.get(path, {
29
+ signal: AbortSignal.timeout(timeoutMs)
30
+ })
31
+ );
32
+ }
33
+
34
+ // src/hooks/decisions-check.ts
35
+ var DECISIONS_CHECK_TIMEOUT_MS = 1e4;
36
+ var MAX_FILES_PER_REQUEST = 25;
37
+ var defaultDeps = { getClient };
38
+ function chunk(items, size) {
39
+ const out = [];
40
+ for (let i = 0; i < items.length; i += size) {
41
+ out.push(items.slice(i, i + size));
42
+ }
43
+ return out;
44
+ }
45
+ async function fetchAffecting(client, batch) {
46
+ const params = new URLSearchParams();
47
+ for (const file of batch) {
48
+ params.append("files", file);
49
+ }
50
+ try {
51
+ return await daemonOrDirectGet(
52
+ "decisions_affecting",
53
+ `/api/cli/decisions/affecting?${params.toString()}`,
54
+ client,
55
+ DECISIONS_CHECK_TIMEOUT_MS
56
+ );
57
+ } catch (err) {
58
+ const detail = err instanceof Error ? err.message : String(err);
59
+ return { decisions: [], truncated: false, unavailable: `decision check failed: ${detail}` };
60
+ }
61
+ }
62
+ async function checkAffectedDecisions(filePaths, deps = defaultDeps) {
63
+ if (filePaths.length === 0) {
64
+ return { decisions: [], truncated: false };
65
+ }
66
+ const client = deps.getClient();
67
+ const responses = await Promise.all(
68
+ chunk(filePaths, MAX_FILES_PER_REQUEST).map((batch) => fetchAffecting(client, batch))
69
+ );
70
+ const byId = /* @__PURE__ */ new Map();
71
+ let truncated = false;
72
+ let unavailable;
73
+ for (const res of responses) {
74
+ if (res.unavailable !== void 0 && unavailable === void 0) {
75
+ unavailable = res.unavailable;
76
+ }
77
+ truncated ||= res.truncated === true;
78
+ for (const d of res.decisions) {
79
+ const existing = byId.get(d.id);
80
+ if (existing) {
81
+ existing.matchedFiles = [.../* @__PURE__ */ new Set([...existing.matchedFiles, ...d.matchedFiles])];
82
+ } else {
83
+ byId.set(d.id, { ...d, matchedFiles: [...d.matchedFiles] });
84
+ }
85
+ }
86
+ }
87
+ const result = { decisions: [...byId.values()], truncated };
88
+ if (unavailable !== void 0) {
89
+ result.unavailable = unavailable;
90
+ }
91
+ return result;
92
+ }
93
+ var FILES_PREVIEW_LIMIT = 3;
94
+ function formatDecisionsWarning(result) {
95
+ const lines = [];
96
+ if (result.unavailable !== void 0) {
97
+ lines.push(`[prim] decision check not verified \u2014 ${terminalSafeLine(result.unavailable)}`);
98
+ }
99
+ if (result.decisions.length > 0) {
100
+ lines.push(
101
+ `[prim] ${String(result.decisions.length)} active decision(s) reference staged files:`
102
+ );
103
+ for (const d of result.decisions) {
104
+ const statusMark = d.status === "under_review" ? " (under review)" : "";
105
+ const preview = d.matchedFiles.slice(0, FILES_PREVIEW_LIMIT).map(terminalSafeLine).join(", ");
106
+ const overflow = d.matchedFiles.length > FILES_PREVIEW_LIMIT ? ` (+${String(d.matchedFiles.length - FILES_PREVIEW_LIMIT)} more)` : "";
107
+ lines.push(` \xB7 ${terminalSafeLine(d.intent)}${statusMark}`);
108
+ lines.push(` files: ${preview}${overflow}`);
109
+ if (d.rationale) {
110
+ lines.push(` rationale: ${terminalSafeLine(d.rationale)}`);
111
+ }
112
+ }
113
+ }
114
+ if (result.truncated) {
115
+ lines.push(
116
+ "[prim] result truncated \u2014 more files than the server checks per request; not all decisions shown"
117
+ );
118
+ }
119
+ return lines.join("\n");
120
+ }
121
+
122
+ export {
123
+ daemonOrDirectGet,
124
+ checkAffectedDecisions,
125
+ formatDecisionsWarning
126
+ };