pi-gsd 2.0.0 → 2.0.2

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.
@@ -0,0 +1,1532 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // .gsd/extensions/pi-gsd-hooks.ts
31
+ var pi_gsd_hooks_exports = {};
32
+ __export(pi_gsd_hooks_exports, {
33
+ default: () => pi_gsd_hooks_default
34
+ });
35
+ module.exports = __toCommonJS(pi_gsd_hooks_exports);
36
+ var import_node_child_process2 = require("child_process");
37
+ var import_node_fs3 = require("fs");
38
+ var import_node_os = require("os");
39
+ var import_node_path4 = require("path");
40
+
41
+ // src/wxp/index.ts
42
+ var import_node_fs2 = __toESM(require("fs"));
43
+ var import_node_path3 = __toESM(require("path"));
44
+
45
+ // src/wxp/parser.ts
46
+ function extractCodeFenceRegions(content) {
47
+ const regions = [];
48
+ const re = /^```[^\n]*\n[\s\S]*?^```/gm;
49
+ let m;
50
+ while ((m = re.exec(content)) !== null) {
51
+ regions.push([m.index, m.index + m[0].length]);
52
+ }
53
+ return regions;
54
+ }
55
+ function inDeadZone(pos, regions) {
56
+ return regions.some(([s, e]) => pos >= s && pos < e);
57
+ }
58
+ function parseAttrs(raw) {
59
+ const attrs = {};
60
+ const re = /([a-zA-Z0-9_:-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s/>]*)))?/g;
61
+ let m;
62
+ while ((m = re.exec(raw)) !== null) {
63
+ const key = m[1];
64
+ const val = m[2] ?? m[3] ?? m[4] ?? "";
65
+ attrs[key] = val;
66
+ }
67
+ return attrs;
68
+ }
69
+ function parseElement(content, pos) {
70
+ if (content[pos] !== "<") return null;
71
+ const tagRe = /^<([a-zA-Z0-9_:-]+)((?:\s+[a-zA-Z0-9_:-]+(?:=(?:"[^"]*"|'[^']*'|[^\s/>]*))?)*)?\s*(\/??>)/;
72
+ const slice = content.slice(pos);
73
+ const m = tagRe.exec(slice);
74
+ if (!m) return null;
75
+ const tag = m[1];
76
+ const rawAttrs = (m[2] ?? "").trim();
77
+ const closing = m[3];
78
+ const attrs = parseAttrs(rawAttrs);
79
+ if (closing === "/>") {
80
+ return {
81
+ node: { tag, attrs, children: [], selfClosing: true },
82
+ end: pos + m[0].length
83
+ };
84
+ }
85
+ let cursor = pos + m[0].length;
86
+ const children = [];
87
+ const closeTag = `</${tag}>`;
88
+ while (cursor < content.length) {
89
+ const nextOpen = content.indexOf("<", cursor);
90
+ if (nextOpen === -1) break;
91
+ if (content.startsWith(closeTag, nextOpen)) {
92
+ return {
93
+ node: { tag, attrs, children, selfClosing: false },
94
+ end: nextOpen + closeTag.length
95
+ };
96
+ }
97
+ if (content.startsWith("<!--", nextOpen)) {
98
+ const commentEnd = content.indexOf("-->", nextOpen + 4);
99
+ cursor = commentEnd !== -1 ? commentEnd + 3 : content.length;
100
+ continue;
101
+ }
102
+ const child = parseElement(content, nextOpen);
103
+ if (child) {
104
+ children.push(child.node);
105
+ cursor = child.end;
106
+ } else {
107
+ cursor = nextOpen + 1;
108
+ }
109
+ }
110
+ return {
111
+ node: { tag, attrs, children, selfClosing: false },
112
+ end: cursor
113
+ };
114
+ }
115
+ var WXP_TOP_TAGS = /* @__PURE__ */ new Set([
116
+ "gsd-execute",
117
+ "gsd-arguments",
118
+ "gsd-paste",
119
+ "gsd-include",
120
+ "gsd-version"
121
+ ]);
122
+ function extractWxpTags(content) {
123
+ const deadZones = extractCodeFenceRegions(content);
124
+ const matches = [];
125
+ const tagStartRe = /<(gsd-[a-zA-Z0-9_-]+)/g;
126
+ let m;
127
+ while ((m = tagStartRe.exec(content)) !== null) {
128
+ const pos = m.index;
129
+ if (inDeadZone(pos, deadZones)) continue;
130
+ const tagName = m[1];
131
+ if (!WXP_TOP_TAGS.has(tagName)) continue;
132
+ const result = parseElement(content, pos);
133
+ if (!result) continue;
134
+ matches.push({ node: result.node, start: pos, end: result.end });
135
+ tagStartRe.lastIndex = result.end;
136
+ }
137
+ return matches;
138
+ }
139
+ function spliceContent(content, start, end, replacement) {
140
+ return content.slice(0, start) + replacement + content.slice(end);
141
+ }
142
+
143
+ // src/wxp/variables.ts
144
+ function createVariableStore() {
145
+ const scalars = /* @__PURE__ */ new Map();
146
+ const arrays = /* @__PURE__ */ new Map();
147
+ const resolveScalar = (name) => {
148
+ const direct = scalars.get(name)?.value;
149
+ if (direct !== void 0) return direct;
150
+ const dotIdx = name.indexOf(".");
151
+ if (dotIdx === -1) return void 0;
152
+ const varPart = name.slice(0, dotIdx);
153
+ const pathPart = name.slice(dotIdx + 1);
154
+ const jsonStr = scalars.get(varPart)?.value;
155
+ if (jsonStr === void 0) return void 0;
156
+ try {
157
+ let obj = JSON.parse(jsonStr);
158
+ for (const key of pathPart.split(".")) {
159
+ if (obj === null || typeof obj !== "object") return void 0;
160
+ obj = obj[key];
161
+ }
162
+ return obj === void 0 || obj === null ? void 0 : String(obj);
163
+ } catch {
164
+ return void 0;
165
+ }
166
+ };
167
+ return {
168
+ set(name, value, owner) {
169
+ const existing = scalars.get(name);
170
+ if (existing?.owner && owner && existing.owner !== owner) {
171
+ scalars.delete(name);
172
+ scalars.set(`${existing.owner}:${name}`, {
173
+ name: `${existing.owner}:${name}`,
174
+ value: existing.value,
175
+ owner: existing.owner
176
+ });
177
+ scalars.set(`${owner}:${name}`, { name: `${owner}:${name}`, value, owner });
178
+ } else {
179
+ scalars.set(name, { name, value, owner });
180
+ }
181
+ },
182
+ get(name) {
183
+ return scalars.get(name)?.value;
184
+ },
185
+ resolve(name) {
186
+ return resolveScalar(name);
187
+ },
188
+ setArray(name, items, owner) {
189
+ arrays.set(name, items);
190
+ scalars.set(name, { name, value: JSON.stringify(items), owner });
191
+ },
192
+ getArray(name) {
193
+ if (arrays.has(name)) return arrays.get(name);
194
+ const str = scalars.get(name)?.value;
195
+ if (!str) return void 0;
196
+ try {
197
+ const parsed = JSON.parse(str);
198
+ if (Array.isArray(parsed)) return parsed.map(
199
+ (item) => typeof item === "string" ? item : JSON.stringify(item)
200
+ );
201
+ } catch {
202
+ }
203
+ return void 0;
204
+ },
205
+ has(name) {
206
+ return scalars.has(name) || arrays.has(name);
207
+ },
208
+ entries() {
209
+ return scalars.entries();
210
+ },
211
+ snapshot() {
212
+ const out = {};
213
+ for (const [k, v] of scalars) out[k] = v.value;
214
+ return out;
215
+ }
216
+ };
217
+ }
218
+
219
+ // src/wxp/arguments.ts
220
+ var WxpArgumentsError = class extends Error {
221
+ constructor(message) {
222
+ super(message);
223
+ this.name = "WxpArgumentsError";
224
+ }
225
+ };
226
+ function parseArguments(node, rawArguments, vars) {
227
+ const settingsNode = node.children.find((c) => c.tag === "settings");
228
+ const keepExtraArgs = settingsNode?.children.some((c) => c.tag === "keep-extra-args") ?? false;
229
+ const strictArgs = settingsNode?.children.some((c) => c.tag === "strict-args") ?? false;
230
+ const delimContainer = settingsNode?.children.find((c) => c.tag === "delimiters");
231
+ const firstDelim = delimContainer?.children.find((c) => c.tag === "delimiter");
232
+ let tokens;
233
+ if (firstDelim) {
234
+ const raw = firstDelim.attrs["value"] ?? "";
235
+ const sep = raw === "\\n" ? "\n" : raw;
236
+ tokens = rawArguments.split(sep).map((t) => t.trim()).filter(Boolean);
237
+ } else {
238
+ tokens = rawArguments.trim().split(/\s+/).filter(Boolean);
239
+ }
240
+ const argDefs = node.children.filter((c) => c.tag === "arg");
241
+ const consumed = /* @__PURE__ */ new Set();
242
+ for (const def of argDefs.filter((a) => a.attrs["type"] === "flag")) {
243
+ const flagToken = def.attrs["flag"] ?? `--${def.attrs["name"]}`;
244
+ const idx = tokens.indexOf(flagToken);
245
+ const name = def.attrs["name"];
246
+ if (!name) continue;
247
+ if (idx === -1) {
248
+ vars.set(name, "false", void 0);
249
+ } else {
250
+ vars.set(name, "true", void 0);
251
+ consumed.add(idx);
252
+ }
253
+ }
254
+ const positionals = argDefs.filter((a) => a.attrs["type"] !== "flag");
255
+ const remaining = tokens.filter((_, i) => !consumed.has(i));
256
+ let tokenIdx = 0;
257
+ for (let i = 0; i < positionals.length; i++) {
258
+ const def = positionals[i];
259
+ const name = def.attrs["name"];
260
+ const type = def.attrs["type"] ?? "string";
261
+ const isLast = i === positionals.length - 1;
262
+ if (!name) continue;
263
+ if (tokenIdx >= remaining.length) {
264
+ if (!("optional" in def.attrs)) {
265
+ throw new WxpArgumentsError(`Missing required argument '${name}' (type: ${type})`);
266
+ }
267
+ vars.set(name, "", void 0);
268
+ continue;
269
+ }
270
+ if (type === "string" && isLast) {
271
+ vars.set(name, remaining.slice(tokenIdx).join(" "), void 0);
272
+ tokenIdx = remaining.length;
273
+ } else if (type === "number") {
274
+ const raw = remaining[tokenIdx++];
275
+ const num = Number(raw);
276
+ if (isNaN(num)) throw new WxpArgumentsError(`Argument '${name}' expected a number, got '${raw}'`);
277
+ vars.set(name, String(num), void 0);
278
+ } else if (type === "boolean") {
279
+ const raw = remaining[tokenIdx++].toLowerCase();
280
+ if (raw !== "true" && raw !== "false") {
281
+ throw new WxpArgumentsError(`Argument '${name}' expected true/false, got '${raw}'`);
282
+ }
283
+ vars.set(name, raw, void 0);
284
+ } else {
285
+ vars.set(name, remaining[tokenIdx++] ?? "", void 0);
286
+ }
287
+ }
288
+ const extra = remaining.slice(tokenIdx).join(" ");
289
+ if (extra) {
290
+ if (strictArgs) throw new WxpArgumentsError(`Unexpected extra arguments: '${extra}'`);
291
+ if (keepExtraArgs) vars.set("_extra", extra, void 0);
292
+ }
293
+ }
294
+
295
+ // src/wxp/executor.ts
296
+ var import_node_fs = __toESM(require("fs"));
297
+ var import_node_path2 = __toESM(require("path"));
298
+
299
+ // src/wxp/shell.ts
300
+ var import_node_child_process = require("child_process");
301
+
302
+ // src/wxp/security.ts
303
+ var import_node_path = __toESM(require("path"));
304
+ var DEFAULT_SHELL_ALLOWLIST = [
305
+ "pi-gsd-tools",
306
+ "git",
307
+ "node",
308
+ "cat",
309
+ "ls",
310
+ "echo",
311
+ "find"
312
+ ];
313
+ function resolveTrustedEntry(entry, projectRoot, pkgRoot) {
314
+ switch (entry.position) {
315
+ case "project":
316
+ return import_node_path.default.resolve(projectRoot, entry.path);
317
+ case "pkg":
318
+ return import_node_path.default.resolve(pkgRoot, entry.path);
319
+ case "absolute":
320
+ return import_node_path.default.resolve(entry.path);
321
+ }
322
+ }
323
+ function checkTrustedPath(filePath, config, projectRoot, pkgRoot) {
324
+ const resolved = import_node_path.default.resolve(filePath);
325
+ const planningSegment = `${import_node_path.default.sep}.planning`;
326
+ if (resolved.includes(`${planningSegment}${import_node_path.default.sep}`) || resolved.endsWith(planningSegment)) {
327
+ return {
328
+ ok: false,
329
+ reason: ".planning/ files are never processed by WXP (hard security invariant)"
330
+ };
331
+ }
332
+ for (const entry of config.untrustedPaths) {
333
+ const untrustedAbs = resolveTrustedEntry(entry, projectRoot, pkgRoot);
334
+ if (resolved.startsWith(untrustedAbs + import_node_path.default.sep) || resolved === untrustedAbs) {
335
+ return { ok: false, reason: `File '${filePath}' is in an explicitly untrusted path: ${untrustedAbs}` };
336
+ }
337
+ }
338
+ for (const entry of config.trustedPaths) {
339
+ const trustedAbs = resolveTrustedEntry(entry, projectRoot, pkgRoot);
340
+ if (resolved.startsWith(trustedAbs + import_node_path.default.sep) || resolved === trustedAbs) {
341
+ return { ok: true };
342
+ }
343
+ }
344
+ return {
345
+ ok: false,
346
+ reason: `File '${filePath}' is not in a trusted WXP path.`
347
+ };
348
+ }
349
+ function checkAllowlist(command, config) {
350
+ const bare = import_node_path.default.basename(command);
351
+ if (config.shellBanlist.includes(bare)) {
352
+ return { ok: false, reason: `Command '${bare}' is explicitly banned by WXP security config.` };
353
+ }
354
+ if (config.shellAllowlist.includes(bare)) {
355
+ return { ok: true };
356
+ }
357
+ return {
358
+ ok: false,
359
+ reason: `Command '${bare}' is not in the WXP shell allowlist. Allowed: ${config.shellAllowlist.join(", ")}`
360
+ };
361
+ }
362
+
363
+ // src/wxp/shell.ts
364
+ var WxpShellError = class extends Error {
365
+ constructor(command, stderr, variableSnapshot, message) {
366
+ super(message);
367
+ this.command = command;
368
+ this.stderr = stderr;
369
+ this.variableSnapshot = variableSnapshot;
370
+ this.name = "WxpShellError";
371
+ }
372
+ command;
373
+ stderr;
374
+ variableSnapshot;
375
+ };
376
+ function resolveArgNode(arg, vars) {
377
+ if (arg.attrs["string"] !== void 0) return arg.attrs["string"];
378
+ if (arg.attrs["name"] !== void 0) {
379
+ const raw = vars.resolve(arg.attrs["name"]) ?? "";
380
+ const wrap = arg.attrs["wrap"];
381
+ return wrap ? `${wrap}${raw}${wrap}` : raw;
382
+ }
383
+ if (arg.attrs["value"] !== void 0) return arg.attrs["value"];
384
+ return "";
385
+ }
386
+ function executeShell(node, vars, config) {
387
+ const command = node.attrs["command"] ?? "";
388
+ const check = checkAllowlist(command, config);
389
+ if (!check.ok) {
390
+ throw new WxpShellError(command, "", vars.snapshot(), check.reason);
391
+ }
392
+ const argsContainer = node.children.find((c) => c.tag === "args");
393
+ const outsContainer = node.children.find((c) => c.tag === "outs");
394
+ const resolvedArgs = argsContainer ? argsContainer.children.filter((c) => c.tag === "arg").map((a) => resolveArgNode(a, vars)) : [];
395
+ const suppressErrors = outsContainer ? outsContainer.children.some((c) => c.tag === "suppress-errors") : false;
396
+ const outVars = outsContainer ? outsContainer.children.filter((c) => c.tag === "out" && c.attrs["name"]).map((c) => c.attrs["name"]) : [];
397
+ let stdout = "";
398
+ try {
399
+ stdout = (0, import_node_child_process.execFileSync)(command, resolvedArgs, {
400
+ encoding: "utf8",
401
+ timeout: config.shellTimeoutMs,
402
+ windowsHide: true
403
+ }).trim();
404
+ } catch (err) {
405
+ if (suppressErrors) {
406
+ for (const name of outVars) vars.set(name, "", void 0);
407
+ return;
408
+ }
409
+ const e = err;
410
+ const stderr = (e.stderr ?? e.message ?? String(err)).trim();
411
+ throw new WxpShellError(
412
+ command,
413
+ stderr,
414
+ vars.snapshot(),
415
+ `Shell '${command} ${resolvedArgs.join(" ")}' failed: ${stderr}`
416
+ );
417
+ }
418
+ if (outVars.length > 0) vars.set(outVars[0], stdout, void 0);
419
+ }
420
+
421
+ // src/wxp/string-ops.ts
422
+ var WxpStringOpError = class extends Error {
423
+ constructor(message) {
424
+ super(message);
425
+ this.name = "WxpStringOpError";
426
+ }
427
+ };
428
+ function executeStringOp(node, vars) {
429
+ const op = node.attrs["op"];
430
+ if (op !== "split") throw new WxpStringOpError(`<string-op> only op="split" is supported in v1`);
431
+ const argsContainer = node.children.find((c) => c.tag === "args");
432
+ const outsContainer = node.children.find((c) => c.tag === "outs");
433
+ if (!argsContainer || !outsContainer) {
434
+ throw new WxpStringOpError(`<string-op> requires <args> and <outs>`);
435
+ }
436
+ const args = argsContainer.children.filter((c) => c.tag === "arg");
437
+ const outs = outsContainer.children.filter((c) => c.tag === "out");
438
+ const srcArg = args[0];
439
+ const delimArg = args[1];
440
+ if (!srcArg) throw new WxpStringOpError(`<string-op op="split"> requires at least 2 <arg> children`);
441
+ const source = resolveArgNode(srcArg, vars);
442
+ if (srcArg.attrs["name"] && vars.get(srcArg.attrs["name"]) === void 0) {
443
+ throw new WxpStringOpError(`string-op split: source variable '${srcArg.attrs["name"]}' is not defined`);
444
+ }
445
+ const delimiter = delimArg ? resolveArgNode(delimArg, vars) : "";
446
+ const parts = source.split(delimiter);
447
+ outs.forEach((out, i) => {
448
+ const name = out.attrs["name"];
449
+ if (name) vars.set(name, parts[i + 1] ?? parts[i] ?? "", void 0);
450
+ });
451
+ }
452
+
453
+ // src/wxp/conditions.ts
454
+ var BINARY_OPS = /* @__PURE__ */ new Set([
455
+ "equals",
456
+ "not-equals",
457
+ "starts-with",
458
+ "contains",
459
+ "less-than",
460
+ "greater-than",
461
+ "less-than-or-equal",
462
+ "greater-than-or-equal"
463
+ ]);
464
+ var CONDITION_OPS = /* @__PURE__ */ new Set([...BINARY_OPS, "and", "or"]);
465
+ function resolveOperand(node, vars) {
466
+ if (node.attrs["name"]) return vars.resolve(node.attrs["name"]) ?? "";
467
+ if (node.attrs["value"] !== void 0) return node.attrs["value"];
468
+ return "";
469
+ }
470
+ function isNumeric(node) {
471
+ return node.attrs["type"] === "number";
472
+ }
473
+ function evalBinary(node, vars) {
474
+ const leftNode = node.children.find((c) => c.tag === "left");
475
+ const rightNode = node.children.find((c) => c.tag === "right");
476
+ if (!leftNode || !rightNode) return false;
477
+ const numeric = isNumeric(leftNode) || isNumeric(rightNode);
478
+ if (numeric) {
479
+ const l2 = Number(resolveOperand(leftNode, vars));
480
+ const r2 = Number(resolveOperand(rightNode, vars));
481
+ switch (node.tag) {
482
+ case "equals":
483
+ return l2 === r2;
484
+ case "not-equals":
485
+ return l2 !== r2;
486
+ case "less-than":
487
+ return l2 < r2;
488
+ case "greater-than":
489
+ return l2 > r2;
490
+ case "less-than-or-equal":
491
+ return l2 <= r2;
492
+ case "greater-than-or-equal":
493
+ return l2 >= r2;
494
+ default:
495
+ return false;
496
+ }
497
+ }
498
+ const l = resolveOperand(leftNode, vars);
499
+ const r = resolveOperand(rightNode, vars);
500
+ switch (node.tag) {
501
+ case "equals":
502
+ return l === r;
503
+ case "not-equals":
504
+ return l !== r;
505
+ case "starts-with":
506
+ return l.startsWith(r);
507
+ case "contains":
508
+ return l.includes(r);
509
+ case "less-than":
510
+ return Number(l) < Number(r);
511
+ case "greater-than":
512
+ return Number(l) > Number(r);
513
+ case "less-than-or-equal":
514
+ return Number(l) <= Number(r);
515
+ case "greater-than-or-equal":
516
+ return Number(l) >= Number(r);
517
+ default:
518
+ return false;
519
+ }
520
+ }
521
+ function evaluateCondExprNode(node, vars) {
522
+ if (node.tag === "and") {
523
+ return node.children.filter((c) => CONDITION_OPS.has(c.tag)).every((c) => evaluateCondExprNode(c, vars));
524
+ }
525
+ if (node.tag === "or") {
526
+ return node.children.filter((c) => CONDITION_OPS.has(c.tag)).some((c) => evaluateCondExprNode(c, vars));
527
+ }
528
+ return evalBinary(node, vars);
529
+ }
530
+ function evaluateCondition(ifNode, vars) {
531
+ const condContainer = ifNode.children.find((c) => c.tag === "condition");
532
+ if (!condContainer) return false;
533
+ const exprNode = condContainer.children.find((c) => CONDITION_OPS.has(c.tag));
534
+ return exprNode ? evaluateCondExprNode(exprNode, vars) : false;
535
+ }
536
+ function evaluateWhere(whereNode, vars) {
537
+ const exprNode = whereNode.children.find((c) => CONDITION_OPS.has(c.tag));
538
+ return exprNode ? evaluateCondExprNode(exprNode, vars) : true;
539
+ }
540
+
541
+ // src/wxp/executor.ts
542
+ var WxpExecutionError = class extends Error {
543
+ constructor(cause, variableSnapshot, message) {
544
+ super(message);
545
+ this.cause = cause;
546
+ this.variableSnapshot = variableSnapshot;
547
+ this.name = "WxpExecutionError";
548
+ }
549
+ cause;
550
+ variableSnapshot;
551
+ };
552
+ function execDisplay(node, vars, ctx) {
553
+ const msg = (node.attrs["msg"] ?? "").replace(
554
+ /\{([^}]+)\}/g,
555
+ (_, name) => vars.resolve(name) ?? ""
556
+ );
557
+ const level = node.attrs["level"];
558
+ ctx.onDisplay(msg, level === "warning" || level === "error" ? level : "info");
559
+ }
560
+ function execJsonParse(node, vars) {
561
+ const src = node.attrs["src"] ?? "";
562
+ const out = node.attrs["out"] ?? "";
563
+ const pathStr = node.attrs["path"];
564
+ const jsonStr = vars.get(src);
565
+ if (jsonStr === void 0) throw new Error(`<json-parse>: source variable '${src}' is not defined`);
566
+ let parsed;
567
+ try {
568
+ parsed = JSON.parse(jsonStr);
569
+ } catch {
570
+ throw new Error(`<json-parse>: '${src}' does not contain valid JSON`);
571
+ }
572
+ if (pathStr) {
573
+ const parts = pathStr.replace(/^\$\.?/, "").split(".");
574
+ let cur = parsed;
575
+ for (const key of parts) {
576
+ if (cur === null || typeof cur !== "object") throw new Error(`<json-parse>: path '${pathStr}' not found`);
577
+ cur = cur[key];
578
+ }
579
+ parsed = cur;
580
+ }
581
+ if (Array.isArray(parsed)) {
582
+ vars.setArray(out, parsed.map((item) => typeof item === "string" ? item : JSON.stringify(item)));
583
+ } else if (parsed !== null && typeof parsed === "object") {
584
+ vars.set(out, JSON.stringify(parsed), void 0);
585
+ } else {
586
+ vars.set(out, parsed === void 0 || parsed === null ? "" : String(parsed), void 0);
587
+ }
588
+ }
589
+ function execReadFile(node, vars) {
590
+ const filePath = node.attrs["path"] ?? "";
591
+ const out = node.attrs["out"] ?? "";
592
+ const content = import_node_fs.default.readFileSync(import_node_path2.default.resolve(filePath), "utf8");
593
+ vars.set(out, content, void 0);
594
+ }
595
+ function execWriteFile(node, vars, ctx) {
596
+ const filePath = node.attrs["path"] ?? "";
597
+ const src = node.attrs["src"] ?? "";
598
+ const resolved = import_node_path2.default.resolve(filePath);
599
+ if (import_node_fs.default.existsSync(resolved)) {
600
+ throw new Error(`<write-file>: '${filePath}' already exists (create-only, never overwrites)`);
601
+ }
602
+ for (const entry of ctx.config.trustedPaths) {
603
+ const abs = resolveTrustedEntry(entry, ctx.projectRoot, ctx.pkgRoot);
604
+ if (resolved.startsWith(abs + import_node_path2.default.sep) || resolved === abs) {
605
+ throw new Error(`<write-file>: cannot write to trusted harness path '${filePath}'`);
606
+ }
607
+ }
608
+ const content = vars.get(src) ?? "";
609
+ import_node_fs.default.mkdirSync(import_node_path2.default.dirname(resolved), { recursive: true });
610
+ import_node_fs.default.writeFileSync(resolved, content, "utf8");
611
+ }
612
+ function execForEach(node, vars, ctx) {
613
+ const varName = node.attrs["var"] ?? "";
614
+ const itemName = node.attrs["item"] ?? "";
615
+ const whereNode = node.children.find((c) => c.tag === "where");
616
+ const sortByNode = node.children.find((c) => c.tag === "sort-by");
617
+ const bodyNodes = node.children.filter((c) => c.tag !== "where" && c.tag !== "sort-by");
618
+ let items = vars.getArray(varName);
619
+ if (!items) return;
620
+ if (whereNode) {
621
+ items = items.filter((itemJson) => {
622
+ vars.set(itemName, itemJson, void 0);
623
+ return evaluateWhere(whereNode, vars);
624
+ });
625
+ }
626
+ if (sortByNode) {
627
+ const key = sortByNode.attrs["key"] ?? "";
628
+ const type = sortByNode.attrs["type"] ?? "string";
629
+ const order = sortByNode.attrs["order"] ?? "asc";
630
+ items = [...items].sort((aJson, bJson) => {
631
+ vars.set(itemName, aJson, void 0);
632
+ const aVal = vars.resolve(`${itemName}.${key}`) ?? vars.resolve(key) ?? "";
633
+ vars.set(itemName, bJson, void 0);
634
+ const bVal = vars.resolve(`${itemName}.${key}`) ?? vars.resolve(key) ?? "";
635
+ const cmp = type === "number" ? Number(aVal) - Number(bVal) : aVal.localeCompare(bVal);
636
+ return order === "desc" ? -cmp : cmp;
637
+ });
638
+ }
639
+ for (const itemJson of items) {
640
+ vars.set(itemName, itemJson, void 0);
641
+ for (const child of bodyNodes) executeNode(child, vars, ctx);
642
+ }
643
+ }
644
+ function executeNode(node, vars, ctx) {
645
+ switch (node.tag) {
646
+ case "shell":
647
+ executeShell(node, vars, ctx.config);
648
+ break;
649
+ case "string-op":
650
+ executeStringOp(node, vars);
651
+ break;
652
+ case "json-parse":
653
+ execJsonParse(node, vars);
654
+ break;
655
+ case "read-file":
656
+ execReadFile(node, vars);
657
+ break;
658
+ case "write-file":
659
+ execWriteFile(node, vars, ctx);
660
+ break;
661
+ case "display":
662
+ execDisplay(node, vars, ctx);
663
+ break;
664
+ case "for-each":
665
+ execForEach(node, vars, ctx);
666
+ break;
667
+ case "if": {
668
+ const branch = evaluateCondition(node, vars);
669
+ const thenNode = node.children.find((c) => c.tag === "then");
670
+ const elseNode = node.children.find((c) => c.tag === "else");
671
+ const taken = branch ? thenNode : elseNode;
672
+ if (taken) for (const child of taken.children) executeNode(child, vars, ctx);
673
+ break;
674
+ }
675
+ case "gsd-execute":
676
+ executeBlock(node, vars, ctx);
677
+ break;
678
+ default:
679
+ break;
680
+ }
681
+ }
682
+ function executeBlock(node, vars, ctx) {
683
+ try {
684
+ for (const child of node.children) executeNode(child, vars, ctx);
685
+ } catch (err) {
686
+ if (err instanceof WxpShellError || err instanceof Error) {
687
+ throw new WxpExecutionError(err, vars.snapshot(), `Execution failed: ${err.message}`);
688
+ }
689
+ throw err;
690
+ }
691
+ }
692
+
693
+ // src/wxp/paste.ts
694
+ var WxpPasteError = class extends Error {
695
+ constructor(variableName, variableSnapshot) {
696
+ super(
697
+ `<gsd-paste name="${variableName}" /> references undefined variable '${variableName}'`
698
+ );
699
+ this.variableName = variableName;
700
+ this.variableSnapshot = variableSnapshot;
701
+ this.name = "WxpPasteError";
702
+ }
703
+ variableName;
704
+ variableSnapshot;
705
+ };
706
+ function applyPaste(content, vars) {
707
+ const deadZones = extractCodeFenceRegions(content);
708
+ const pasteRe = /<gsd-paste\s+name="([^"]+)"\s*\/>/g;
709
+ const matches = [];
710
+ let m;
711
+ while ((m = pasteRe.exec(content)) !== null) {
712
+ if (!inDeadZone(m.index, deadZones)) {
713
+ matches.push({ index: m.index, full: m[0], name: m[1] });
714
+ }
715
+ }
716
+ for (const match of matches) {
717
+ if (vars.get(match.name) === void 0) {
718
+ throw new WxpPasteError(match.name, vars.snapshot());
719
+ }
720
+ }
721
+ let result = content;
722
+ for (let i = matches.length - 1; i >= 0; i--) {
723
+ const match = matches[i];
724
+ const value = vars.get(match.name);
725
+ result = result.slice(0, match.index) + value + result.slice(match.index + match.full.length);
726
+ }
727
+ return result;
728
+ }
729
+
730
+ // src/wxp/index.ts
731
+ var MAX_ITERATIONS = 50;
732
+ var NOOP_DISPLAY = () => {
733
+ };
734
+ var WxpProcessingError = class extends Error {
735
+ constructor(filePath, cause, variableSnapshot, pendingOperations, completedOperations) {
736
+ super(
737
+ [
738
+ `WXP Processing Error`,
739
+ `File: ${filePath}`,
740
+ `Error: ${cause.message}`,
741
+ `Variable Namespace: ${JSON.stringify(variableSnapshot, null, 2)}`,
742
+ `Pending Operations: [${pendingOperations.join(", ")}]`,
743
+ `Completed Operations: [${completedOperations.join(", ")}]`
744
+ ].join("\n")
745
+ );
746
+ this.filePath = filePath;
747
+ this.cause = cause;
748
+ this.variableSnapshot = variableSnapshot;
749
+ this.pendingOperations = pendingOperations;
750
+ this.completedOperations = completedOperations;
751
+ this.name = "WxpProcessingError";
752
+ }
753
+ filePath;
754
+ cause;
755
+ variableSnapshot;
756
+ pendingOperations;
757
+ completedOperations;
758
+ };
759
+ function processWxpTrustedContent(content, virtualFilePath, config, projectRoot, pkgRoot, rawArguments = "", onDisplay = NOOP_DISPLAY) {
760
+ const trusted = {
761
+ ...config,
762
+ trustedPaths: [
763
+ ...config.trustedPaths,
764
+ { position: "absolute", path: import_node_path3.default.dirname(import_node_path3.default.resolve(virtualFilePath)) }
765
+ ]
766
+ };
767
+ return runLoop(content, virtualFilePath, trusted, projectRoot, pkgRoot, rawArguments, onDisplay);
768
+ }
769
+ function runLoop(content, filePath, config, projectRoot, pkgRoot, rawArguments, onDisplay) {
770
+ const vars = createVariableStore();
771
+ const done = [];
772
+ let current = content;
773
+ const ctx = { config, projectRoot, pkgRoot, onDisplay };
774
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
775
+ const tags = extractWxpTags(current);
776
+ const active = tags.filter((t) => t.node.tag !== "gsd-version");
777
+ if (active.length === 0) break;
778
+ const pending = active.map((t) => t.node.tag);
779
+ try {
780
+ let progress = false;
781
+ for (const tag of extractWxpTags(current)) {
782
+ if (tag.node.tag !== "gsd-include") continue;
783
+ if (inDeadZone(tag.start, extractCodeFenceRegions(current))) continue;
784
+ const incPath = tag.node.attrs["path"];
785
+ if (!incPath) continue;
786
+ const abs = import_node_path3.default.resolve(import_node_path3.default.dirname(filePath), incPath);
787
+ const check = checkTrustedPath(abs, config, projectRoot, pkgRoot);
788
+ if (!check.ok) throw new Error(`Include rejected: ${check.reason}`);
789
+ const included = import_node_fs2.default.readFileSync(abs, "utf8");
790
+ const stem = import_node_path3.default.basename(abs, import_node_path3.default.extname(abs));
791
+ for (const child of tag.node.children) {
792
+ if (child.tag !== "gsd-arguments") continue;
793
+ for (const arg of child.children.filter((c) => c.tag === "arg")) {
794
+ const from = arg.attrs["name"];
795
+ const to = arg.attrs["as"];
796
+ if (from && to) {
797
+ const val = vars.get(from);
798
+ if (val !== void 0) vars.set(to, val, stem);
799
+ }
800
+ }
801
+ }
802
+ const appendArgs = "include-arguments" in tag.node.attrs ? `
803
+ ${rawArguments}` : "";
804
+ current = spliceContent(current, tag.start, tag.end, included + appendArgs);
805
+ done.push("gsd-include");
806
+ progress = true;
807
+ break;
808
+ }
809
+ if (progress) continue;
810
+ for (const tag of extractWxpTags(current)) {
811
+ if (tag.node.tag !== "gsd-arguments") continue;
812
+ if (inDeadZone(tag.start, extractCodeFenceRegions(current))) continue;
813
+ parseArguments(tag.node, rawArguments, vars);
814
+ current = spliceContent(current, tag.start, tag.end, "");
815
+ done.push("gsd-arguments");
816
+ progress = true;
817
+ break;
818
+ }
819
+ if (progress) continue;
820
+ for (const tag of extractWxpTags(current)) {
821
+ if (tag.node.tag !== "gsd-execute") continue;
822
+ if (inDeadZone(tag.start, extractCodeFenceRegions(current))) continue;
823
+ executeBlock(tag.node, vars, ctx);
824
+ current = spliceContent(current, tag.start, tag.end, "");
825
+ done.push("gsd-execute");
826
+ progress = true;
827
+ break;
828
+ }
829
+ if (progress) continue;
830
+ const after = applyPaste(current, vars);
831
+ if (after !== current) {
832
+ current = after;
833
+ done.push("gsd-paste");
834
+ continue;
835
+ }
836
+ break;
837
+ } catch (err) {
838
+ if (err instanceof WxpProcessingError) throw err;
839
+ const e = err instanceof Error ? err : new Error(String(err));
840
+ throw new WxpProcessingError(filePath, e, vars.snapshot(), pending, done);
841
+ }
842
+ }
843
+ return current;
844
+ }
845
+ function readWorkflowVersionTag(content) {
846
+ const m = /<gsd-version\s+v="([^"]+)"(\s+do-not-update)?\s*\/>/.exec(content);
847
+ if (!m) return null;
848
+ return { version: m[1], doNotUpdate: Boolean(m[2]) };
849
+ }
850
+
851
+ // .gsd/extensions/pi-gsd-hooks.ts
852
+ function copyHarness(src, dest) {
853
+ let symlinksReplaced = 0;
854
+ let filesCopied = 0;
855
+ const walk = (srcDir, destDir) => {
856
+ (0, import_node_fs3.mkdirSync)(destDir, { recursive: true });
857
+ const entries = (0, import_node_fs3.readdirSync)(srcDir, { withFileTypes: true });
858
+ for (const entry of entries) {
859
+ const srcPath = (0, import_node_path4.join)(srcDir, entry.name);
860
+ const destPath = (0, import_node_path4.join)(destDir, entry.name);
861
+ if (entry.isDirectory()) {
862
+ walk(srcPath, destPath);
863
+ continue;
864
+ }
865
+ if ((0, import_node_fs3.existsSync)(destPath)) {
866
+ try {
867
+ const st = (0, import_node_fs3.lstatSync)(destPath);
868
+ if (st.isSymbolicLink()) {
869
+ try {
870
+ const { unlinkSync } = require("fs");
871
+ unlinkSync(destPath);
872
+ } catch {
873
+ }
874
+ (0, import_node_fs3.copyFileSync)(srcPath, destPath);
875
+ symlinksReplaced++;
876
+ }
877
+ } catch {
878
+ }
879
+ continue;
880
+ }
881
+ try {
882
+ (0, import_node_fs3.copyFileSync)(srcPath, destPath);
883
+ filesCopied++;
884
+ } catch {
885
+ }
886
+ }
887
+ };
888
+ walk(src, dest);
889
+ return { symlinksReplaced, filesCopied };
890
+ }
891
+ function extractRawArguments(content) {
892
+ const lastTagEnd = (() => {
893
+ const tagPattern = /<\/(?:gsd-[a-zA-Z0-9_-]+|shell|if|then|else|condition|args|outs|string-op|settings)>/g;
894
+ let lastEnd = 0;
895
+ let m;
896
+ while ((m = tagPattern.exec(content)) !== null) {
897
+ lastEnd = m.index + m[0].length;
898
+ }
899
+ return lastEnd;
900
+ })();
901
+ const trailing = content.slice(lastTagEnd).trim();
902
+ if (trailing.length === 0 || trailing.length > 500 || trailing.includes("\n\n\n")) {
903
+ return "";
904
+ }
905
+ return trailing;
906
+ }
907
+ function pi_gsd_hooks_default(pi) {
908
+ function resolveGsdInclude(match, cwd, pkgHarness, errors) {
909
+ const filePath = match[1];
910
+ const selectExpr = match[2] ?? "";
911
+ const subPath = filePath.replace(/^\.pi\/gsd\//, "");
912
+ const candidates = [
913
+ (0, import_node_path4.join)(cwd, filePath),
914
+ ...filePath.startsWith(".pi/gsd/") && pkgHarness ? [(0, import_node_path4.join)(pkgHarness, subPath)] : []
915
+ ];
916
+ let raw = null;
917
+ for (const c of candidates) {
918
+ try {
919
+ if ((0, import_node_fs3.existsSync)(c)) {
920
+ raw = (0, import_node_fs3.readFileSync)(c, "utf8");
921
+ break;
922
+ }
923
+ } catch {
924
+ }
925
+ }
926
+ if (raw === null) {
927
+ errors.push("File not found: " + filePath);
928
+ return null;
929
+ }
930
+ let result = raw;
931
+ if (!selectExpr) return result;
932
+ const parts = selectExpr.split("|");
933
+ if (parts.length > 2) {
934
+ errors.push("Invalid selector (max 2 segments): " + selectExpr);
935
+ return null;
936
+ }
937
+ if (parts.length > 1 && parts.some((p) => p.trim().startsWith("lines:"))) {
938
+ errors.push("lines: cannot be chained \u2014 use it alone: " + selectExpr);
939
+ return null;
940
+ }
941
+ for (const part of parts) {
942
+ const p = part.trim();
943
+ if (p.startsWith("tag:")) {
944
+ const tagName = p.slice(4);
945
+ const tagRe = new RegExp("<" + tagName + ">([\\s\\S]*?)</" + tagName + ">", "i");
946
+ const tagMatch = result.match(tagRe);
947
+ if (!tagMatch) {
948
+ errors.push("Tag <" + tagName + "> not found in " + filePath);
949
+ return null;
950
+ }
951
+ result = tagMatch[1].trim();
952
+ } else if (p.startsWith("heading:")) {
953
+ const headingText = p.slice(8);
954
+ const escaped = headingText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
955
+ const headingRe = new RegExp("(^|\\n)(#{1,6})\\s+" + escaped + "\\s*\\n");
956
+ const hMatch = result.match(headingRe);
957
+ if (!hMatch) {
958
+ errors.push('Heading "' + headingText + '" not found in ' + filePath);
959
+ return null;
960
+ }
961
+ const level = hMatch[2].length;
962
+ const startIdx = (hMatch.index ?? 0) + hMatch[0].length;
963
+ const nextHeading = result.slice(startIdx).search(new RegExp("\\n#{1," + level + "}\\s"));
964
+ result = nextHeading === -1 ? result.slice(startIdx).trim() : result.slice(startIdx, startIdx + nextHeading).trim();
965
+ } else if (p.startsWith("lines:")) {
966
+ const rangeMatch = p.match(/^lines:(\d+)-(\d+)$/);
967
+ if (!rangeMatch) {
968
+ errors.push("Invalid lines selector: " + p);
969
+ return null;
970
+ }
971
+ const start = parseInt(rangeMatch[1], 10) - 1;
972
+ const end = parseInt(rangeMatch[2], 10);
973
+ result = result.split("\n").slice(start, end).join("\n");
974
+ } else {
975
+ errors.push("Unknown selector: " + p);
976
+ return null;
977
+ }
978
+ }
979
+ return result;
980
+ }
981
+ pi.on("context", async (event, ctx) => {
982
+ const includePattern = /<gsd-include\s+path="([^"]+)"(?:\s+select="([^"]*)")?\s*\/>/g;
983
+ const extFile = typeof __filename !== "undefined" ? __filename : "";
984
+ const pkgHarness = extFile ? (0, import_node_path4.join)((0, import_node_path4.dirname)(extFile), "..", "harnesses", "pi", "get-shit-done") : "";
985
+ const errors = [];
986
+ const messages = event.messages;
987
+ for (const msg of messages) {
988
+ if (msg.role !== "user") continue;
989
+ if (typeof msg.content === "string") {
990
+ const includes = [...msg.content.matchAll(includePattern)];
991
+ if (includes.length === 0) continue;
992
+ let transformed = msg.content;
993
+ for (const match of includes) {
994
+ const replacement = resolveGsdInclude(match, ctx.cwd, pkgHarness, errors);
995
+ if (replacement === null) continue;
996
+ transformed = transformed.replace(match[0], replacement);
997
+ }
998
+ msg.content = transformed;
999
+ } else if (Array.isArray(msg.content)) {
1000
+ for (const block of msg.content) {
1001
+ if (block.type !== "text" || !block.text) continue;
1002
+ const includes = [...block.text.matchAll(includePattern)];
1003
+ if (includes.length === 0) continue;
1004
+ let transformed = block.text;
1005
+ for (const match of includes) {
1006
+ const replacement = resolveGsdInclude(match, ctx.cwd, pkgHarness, errors);
1007
+ if (replacement === null) continue;
1008
+ transformed = transformed.replace(match[0], replacement);
1009
+ }
1010
+ block.text = transformed;
1011
+ }
1012
+ }
1013
+ }
1014
+ if (errors.length > 0) {
1015
+ ctx.ui.notify("\u274C GSD include failed:\n" + errors.map((e) => " \u2022 " + e).join("\n"), "error");
1016
+ return { messages: [] };
1017
+ }
1018
+ const extFile2 = typeof __filename !== "undefined" ? __filename : "";
1019
+ const pkgRoot2 = (0, import_node_path4.join)((0, import_node_path4.dirname)(extFile2), "..", "..");
1020
+ const loadSettings = (settingsPath) => {
1021
+ try {
1022
+ if ((0, import_node_fs3.existsSync)(settingsPath)) {
1023
+ return JSON.parse((0, import_node_fs3.readFileSync)(settingsPath, "utf8"));
1024
+ }
1025
+ } catch {
1026
+ }
1027
+ return {};
1028
+ };
1029
+ const globalSettings = loadSettings((0, import_node_path4.join)((0, import_node_os.homedir)(), ".gsd", "pi-gsd-settings.json"));
1030
+ const projectSettings = loadSettings((0, import_node_path4.join)(ctx.cwd, ".pi", "gsd", "pi-gsd-settings.json"));
1031
+ const mergedAllowlist = [
1032
+ ...DEFAULT_SHELL_ALLOWLIST,
1033
+ ...globalSettings.shellAllowlist ?? [],
1034
+ ...projectSettings.shellAllowlist ?? []
1035
+ ];
1036
+ const wxpSecurity = {
1037
+ trustedPaths: [
1038
+ ...globalSettings.trustedPaths ?? [],
1039
+ ...projectSettings.trustedPaths ?? [],
1040
+ { position: "pkg", path: ".gsd/harnesses/pi/get-shit-done" },
1041
+ { position: "project", path: ".pi/gsd" }
1042
+ ],
1043
+ untrustedPaths: [
1044
+ ...globalSettings.untrustedPaths ?? [],
1045
+ ...projectSettings.untrustedPaths ?? []
1046
+ ],
1047
+ shellAllowlist: [...new Set(mergedAllowlist)],
1048
+ shellBanlist: [
1049
+ ...globalSettings.shellBanlist ?? [],
1050
+ ...projectSettings.shellBanlist ?? []
1051
+ ],
1052
+ shellTimeoutMs: projectSettings.shellTimeoutMs ?? globalSettings.shellTimeoutMs ?? 3e4
1053
+ };
1054
+ try {
1055
+ for (const msg of messages) {
1056
+ if (msg.role !== "user") continue;
1057
+ if (typeof msg.content === "string") {
1058
+ if (!msg.content.includes("<gsd-")) continue;
1059
+ const virtualPath = (0, import_node_path4.join)(ctx.cwd, ".pi", "gsd", "workflows", "_message.md");
1060
+ const rawArgs = extractRawArguments(msg.content);
1061
+ msg.content = processWxpTrustedContent(msg.content, virtualPath, wxpSecurity, ctx.cwd, pkgRoot2, rawArgs, (m, lv) => ctx.ui.notify(m, lv === "error" ? "error" : "info"));
1062
+ } else if (Array.isArray(msg.content)) {
1063
+ for (const block of msg.content) {
1064
+ if (block.type !== "text" || !block.text) continue;
1065
+ if (!block.text.includes("<gsd-")) continue;
1066
+ const virtualPath = (0, import_node_path4.join)(ctx.cwd, ".pi", "gsd", "workflows", "_message.md");
1067
+ const rawArgs = extractRawArguments(block.text);
1068
+ block.text = processWxpTrustedContent(block.text, virtualPath, wxpSecurity, ctx.cwd, pkgRoot2, rawArgs, (m, lv) => ctx.ui.notify(m, lv === "error" ? "error" : "info"));
1069
+ }
1070
+ }
1071
+ }
1072
+ } catch (wxpErr) {
1073
+ if (wxpErr instanceof WxpProcessingError) {
1074
+ ctx.ui.notify(wxpErr.message, "error");
1075
+ return { messages: [] };
1076
+ }
1077
+ const errMsg = wxpErr instanceof Error ? wxpErr.message : String(wxpErr);
1078
+ ctx.ui.notify(`GSD WXP: unexpected context error: ${errMsg}`, "info");
1079
+ }
1080
+ return { messages };
1081
+ });
1082
+ pi.on("session_start", async (_event, ctx) => {
1083
+ try {
1084
+ const extFile = typeof __filename !== "undefined" ? __filename : "";
1085
+ const pkgRoot = (0, import_node_path4.join)((0, import_node_path4.dirname)(extFile), "..", "..");
1086
+ const pkgHarness = (0, import_node_path4.join)(pkgRoot, ".gsd", "harnesses", "pi", "get-shit-done");
1087
+ const projectHarness = (0, import_node_path4.join)(ctx.cwd, ".pi", "gsd");
1088
+ if ((0, import_node_fs3.existsSync)(pkgHarness)) {
1089
+ const { symlinksReplaced } = copyHarness(pkgHarness, projectHarness);
1090
+ if (symlinksReplaced > 0) {
1091
+ ctx.ui.notify(
1092
+ `\u2139\uFE0F GSD: Replaced ${symlinksReplaced} symlink(s) in .pi/gsd/ with real file copies.`,
1093
+ "info"
1094
+ );
1095
+ }
1096
+ try {
1097
+ const pkgJsonPath = (0, import_node_path4.join)(pkgRoot, "package.json");
1098
+ if ((0, import_node_fs3.existsSync)(pkgJsonPath)) {
1099
+ const pkgVersion = JSON.parse((0, import_node_fs3.readFileSync)(pkgJsonPath, "utf8")).version ?? "0.0.0";
1100
+ const outdated = [];
1101
+ const sampleFiles = ["workflows/execute-phase.md", "workflows/plan-phase.md"];
1102
+ for (const rel of sampleFiles) {
1103
+ const projFile = (0, import_node_path4.join)(projectHarness, rel);
1104
+ if (!(0, import_node_fs3.existsSync)(projFile)) continue;
1105
+ const content = (0, import_node_fs3.readFileSync)(projFile, "utf8");
1106
+ const vtag = readWorkflowVersionTag(content);
1107
+ if (!vtag || vtag.doNotUpdate) continue;
1108
+ if (vtag.version !== pkgVersion) outdated.push(rel);
1109
+ }
1110
+ if (outdated.length > 0) {
1111
+ ctx.ui.notify(
1112
+ `\u2139\uFE0F GSD harness update available (package v${pkgVersion}).
1113
+ Outdated files: ${outdated.join(", ")}
1114
+ Run: pi-gsd-tools harness update [y|n|pick|diff]`,
1115
+ "info"
1116
+ );
1117
+ }
1118
+ }
1119
+ } catch {
1120
+ }
1121
+ }
1122
+ } catch {
1123
+ }
1124
+ try {
1125
+ const cacheDir = (0, import_node_path4.join)((0, import_node_os.homedir)(), ".pi", "cache");
1126
+ const cacheFile = (0, import_node_path4.join)(cacheDir, "gsd-update-check.json");
1127
+ const CACHE_TTL_SECONDS = 86400;
1128
+ if ((0, import_node_fs3.existsSync)(cacheFile)) {
1129
+ try {
1130
+ const cache = JSON.parse((0, import_node_fs3.readFileSync)(cacheFile, "utf8"));
1131
+ const ageSeconds = Math.floor(Date.now() / 1e3) - (cache.checked ?? 0);
1132
+ if (cache.update_available && cache.latest) {
1133
+ ctx.ui.notify(
1134
+ `GSD update available: ${cache.installed ?? "?"} \u2192 ${cache.latest}. Run: npm i -g pi-gsd`,
1135
+ "info"
1136
+ );
1137
+ }
1138
+ if (ageSeconds < CACHE_TTL_SECONDS) return;
1139
+ } catch {
1140
+ }
1141
+ }
1142
+ setTimeout(() => {
1143
+ try {
1144
+ (0, import_node_fs3.mkdirSync)(cacheDir, { recursive: true });
1145
+ let installed = "0.0.0";
1146
+ const versionPaths = [
1147
+ (0, import_node_path4.join)(ctx.cwd, ".pi", "gsd", "VERSION"),
1148
+ (0, import_node_path4.join)((0, import_node_os.homedir)(), ".pi", "gsd", "VERSION")
1149
+ ];
1150
+ for (const vp of versionPaths) {
1151
+ if ((0, import_node_fs3.existsSync)(vp)) {
1152
+ try {
1153
+ installed = (0, import_node_fs3.readFileSync)(vp, "utf8").trim();
1154
+ break;
1155
+ } catch {
1156
+ }
1157
+ }
1158
+ }
1159
+ let latest = null;
1160
+ try {
1161
+ latest = (0, import_node_child_process2.execSync)("npm view pi-gsd version", {
1162
+ encoding: "utf8",
1163
+ timeout: 1e4,
1164
+ windowsHide: true
1165
+ }).trim();
1166
+ } catch {
1167
+ }
1168
+ (0, import_node_fs3.writeFileSync)(
1169
+ cacheFile,
1170
+ JSON.stringify({
1171
+ update_available: latest !== null && installed !== "0.0.0" && installed !== latest,
1172
+ installed,
1173
+ latest: latest ?? "unknown",
1174
+ checked: Math.floor(Date.now() / 1e3)
1175
+ })
1176
+ );
1177
+ } catch {
1178
+ }
1179
+ }, 3e3);
1180
+ } catch {
1181
+ }
1182
+ });
1183
+ pi.on("tool_call", async (event, ctx) => {
1184
+ try {
1185
+ if (event.toolName !== "write" && event.toolName !== "edit")
1186
+ return void 0;
1187
+ const filePath = event.input.path ?? "";
1188
+ if (filePath.includes(".planning/")) return void 0;
1189
+ const allowed = [
1190
+ /\.gitignore$/,
1191
+ /\.env/,
1192
+ /AGENTS\.md$/,
1193
+ /settings\.json$/,
1194
+ /pi-gsd-hooks\.ts$/
1195
+ ];
1196
+ if (allowed.some((p) => p.test(filePath))) return void 0;
1197
+ const configPath = (0, import_node_path4.join)(ctx.cwd, ".planning", "config.json");
1198
+ if (!(0, import_node_fs3.existsSync)(configPath)) return void 0;
1199
+ try {
1200
+ const config = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
1201
+ if (!config.hooks?.workflow_guard) return void 0;
1202
+ } catch {
1203
+ return void 0;
1204
+ }
1205
+ const fileName = filePath.split("/").pop() ?? filePath;
1206
+ ctx.ui.notify(
1207
+ `\u26A0\uFE0F GSD: Editing ${fileName} outside a GSD workflow. Consider /gsd-fast or /gsd-quick to maintain state tracking.`,
1208
+ "info"
1209
+ );
1210
+ } catch {
1211
+ }
1212
+ return void 0;
1213
+ });
1214
+ const runJson = (args, cwd) => {
1215
+ try {
1216
+ const raw = (0, import_node_child_process2.execSync)(
1217
+ `pi-gsd-tools ${args} --raw --cwd ${JSON.stringify(cwd)}`,
1218
+ { encoding: "utf8", timeout: 1e4, windowsHide: true }
1219
+ ).trim();
1220
+ return JSON.parse(raw);
1221
+ } catch {
1222
+ return null;
1223
+ }
1224
+ };
1225
+ const bar = (pct, width = 20) => {
1226
+ const filled = Math.round(pct / 100 * width);
1227
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
1228
+ };
1229
+ const cap = (s, max = 42) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1230
+ const nextSteps = (phases) => {
1231
+ const pending = phases.filter((p) => p.status !== "Complete");
1232
+ if (pending.length === 0) {
1233
+ return [
1234
+ " \u2705 All phases complete!",
1235
+ " \u2192 /gsd-audit-milestone Review before archiving",
1236
+ " \u2192 /gsd-complete-milestone Archive and start next"
1237
+ ];
1238
+ }
1239
+ const next = pending[0];
1240
+ const n = next.number;
1241
+ const lines = [` \u23F3 Phase ${n}: ${cap(next.name)}`];
1242
+ if (next.plans === 0) {
1243
+ lines.push(` \u2192 /gsd-discuss-phase ${n} Gather context first`);
1244
+ lines.push(` \u2192 /gsd-plan-phase ${n} Jump straight to planning`);
1245
+ } else if (next.summaries < next.plans) {
1246
+ lines.push(
1247
+ ` \u2192 /gsd-execute-phase ${n} ${next.summaries}/${next.plans} plans done`
1248
+ );
1249
+ } else {
1250
+ lines.push(` \u2192 /gsd-verify-work ${n} All plans done, verify UAT`);
1251
+ }
1252
+ lines.push(` \u2192 /gsd-next Auto-advance`);
1253
+ if (pending.length > 1) {
1254
+ lines.push(
1255
+ ` (+ ${pending.length - 1} more phase${pending.length > 2 ? "s" : ""} pending)`
1256
+ );
1257
+ }
1258
+ return lines;
1259
+ };
1260
+ const formatProgress = (cwd) => {
1261
+ const data = runJson("progress json", cwd);
1262
+ if (!data)
1263
+ return {
1264
+ text: "\u274C No GSD project found. Run /gsd-new-project to initialise.",
1265
+ data: null
1266
+ };
1267
+ const done = data.phases.filter((p) => p.status === "Complete").length;
1268
+ const total = data.phases.length;
1269
+ const phasePct = total > 0 ? Math.round(done / total * 100) : 0;
1270
+ const planPct = data.total_plans > 0 ? Math.round(data.total_summaries / data.total_plans * 100) : 0;
1271
+ const lines = [
1272
+ `\u2501\u2501 GSD Progress \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1273
+ `\u{1F4CB} ${data.milestone_name} (${data.milestone_version})`,
1274
+ ``,
1275
+ `Phases ${bar(phasePct)} ${done}/${total} (${phasePct}%)`,
1276
+ `Plans ${bar(planPct)} ${data.total_summaries}/${data.total_plans} (${planPct}%)`,
1277
+ ``,
1278
+ `Next steps:`,
1279
+ ...nextSteps(data.phases),
1280
+ ``,
1281
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`
1282
+ ];
1283
+ return { text: lines.join("\n"), data };
1284
+ };
1285
+ const formatStats = (cwd) => {
1286
+ const data = runJson("stats json", cwd);
1287
+ if (!data)
1288
+ return {
1289
+ text: "\u274C No GSD project found. Run /gsd-new-project to initialise.",
1290
+ data: null
1291
+ };
1292
+ const reqPct = data.requirements_total > 0 ? Math.round(
1293
+ data.requirements_complete / data.requirements_total * 100
1294
+ ) : 0;
1295
+ const lines = [
1296
+ `\u2501\u2501 GSD Stats \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1297
+ `\u{1F4CB} ${data.milestone_name} (${data.milestone_version})`,
1298
+ ``,
1299
+ `Phases ${bar(data.percent)} ${data.phases_completed}/${data.phases_total} (${data.percent}%)`,
1300
+ `Plans ${bar(data.plan_percent)} ${data.total_summaries}/${data.total_plans} (${data.plan_percent}%)`,
1301
+ `Reqs ${bar(reqPct)} ${data.requirements_complete}/${data.requirements_total} (${reqPct}%)`,
1302
+ ``,
1303
+ `\u{1F5C2} Git commits: ${data.git_commits}`,
1304
+ `\u{1F4C5} Started: ${data.git_first_commit_date}`,
1305
+ `\u{1F4C5} Last activity: ${data.last_activity}`,
1306
+ ``,
1307
+ `Next steps:`,
1308
+ ...nextSteps(data.phases),
1309
+ ``,
1310
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`
1311
+ ];
1312
+ return { text: lines.join("\n"), data };
1313
+ };
1314
+ const formatHealth = (cwd, repair) => {
1315
+ const data = runJson(
1316
+ `validate health${repair ? " --repair" : ""}`,
1317
+ cwd
1318
+ );
1319
+ if (!data)
1320
+ return "\u274C No GSD project found. Run /gsd-new-project to initialise.";
1321
+ const icon = data.status === "ok" ? "\u2705" : data.status === "broken" ? "\u274C" : "\u26A0\uFE0F";
1322
+ const lines = [
1323
+ `\u2501\u2501 GSD Health \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1324
+ `${icon} Status: ${data.status.toUpperCase()}`
1325
+ ];
1326
+ if (data.errors?.length) {
1327
+ lines.push(``, `Errors (${data.errors.length}):`);
1328
+ for (const e of data.errors) {
1329
+ lines.push(` \u2717 [${e.code}] ${e.message}`);
1330
+ if (e.repair) lines.push(` fix: ${e.repair}`);
1331
+ }
1332
+ }
1333
+ if (data.warnings?.length) {
1334
+ lines.push(``, `Warnings (${data.warnings.length}):`);
1335
+ for (const w of data.warnings) {
1336
+ lines.push(` \u26A0 [${w.code}] ${w.message}`);
1337
+ }
1338
+ }
1339
+ if (data.status !== "ok" && !repair) {
1340
+ lines.push(``, ` \u2192 /gsd-health --repair Auto-fix all issues`);
1341
+ }
1342
+ lines.push(``, `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
1343
+ return lines.join("\n");
1344
+ };
1345
+ const nextCommand = (phases) => {
1346
+ const pending = phases.filter((p) => p.status !== "Complete");
1347
+ if (pending.length === 0) return "/gsd-audit-milestone";
1348
+ const next = pending[0];
1349
+ const n = next.number;
1350
+ if (next.plans === 0) return `/gsd-discuss-phase ${n}`;
1351
+ if (next.summaries < next.plans) return `/gsd-execute-phase ${n}`;
1352
+ return `/gsd-verify-work ${n}`;
1353
+ };
1354
+ pi.registerCommand("gsd-progress", {
1355
+ description: "Show project progress with next steps (instant)",
1356
+ handler: async (_args, ctx) => {
1357
+ const { text, data } = formatProgress(ctx.cwd);
1358
+ ctx.ui.notify(text, "info");
1359
+ if (data) {
1360
+ const cmd = nextCommand(data.phases);
1361
+ if (cmd) ctx.ui.setEditorText(cmd);
1362
+ }
1363
+ }
1364
+ });
1365
+ pi.registerCommand("gsd-stats", {
1366
+ description: "Show project statistics (instant)",
1367
+ handler: async (_args, ctx) => {
1368
+ const { text, data } = formatStats(ctx.cwd);
1369
+ ctx.ui.notify(text, "info");
1370
+ if (data) {
1371
+ const cmd = nextCommand(data.phases);
1372
+ if (cmd) ctx.ui.setEditorText(cmd);
1373
+ }
1374
+ }
1375
+ });
1376
+ pi.registerCommand("gsd-health", {
1377
+ description: "Check .planning/ integrity (instant)",
1378
+ handler: async (args, ctx) => {
1379
+ ctx.ui.notify(
1380
+ formatHealth(ctx.cwd, !!args?.includes("--repair")),
1381
+ "info"
1382
+ );
1383
+ },
1384
+ getArgumentCompletions: (prefix) => {
1385
+ const options = [
1386
+ { value: "--repair", label: "--repair Auto-fix issues" }
1387
+ ];
1388
+ return options.filter((o) => o.value.startsWith(prefix));
1389
+ }
1390
+ });
1391
+ pi.registerCommand("gsd-next", {
1392
+ description: "Auto-advance to the next GSD action (instant, no LLM)",
1393
+ handler: async (_args, ctx) => {
1394
+ const data = runJson("progress json", ctx.cwd);
1395
+ if (!data) {
1396
+ ctx.ui.notify(
1397
+ "\u274C No GSD project found. Run /gsd-new-project to initialise.",
1398
+ "error"
1399
+ );
1400
+ ctx.ui.setEditorText("/gsd-new-project");
1401
+ return;
1402
+ }
1403
+ const pending = data.phases.filter((p) => p.status !== "Complete");
1404
+ if (pending.length === 0) {
1405
+ ctx.ui.notify(
1406
+ [
1407
+ `\u2501\u2501 GSD Next \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1408
+ `\u2705 All phases complete!`,
1409
+ `\u2192 /gsd-audit-milestone`,
1410
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`
1411
+ ].join("\n"),
1412
+ "info"
1413
+ );
1414
+ ctx.ui.setEditorText("/gsd-audit-milestone");
1415
+ return;
1416
+ }
1417
+ const next = pending[0];
1418
+ const n = next.number;
1419
+ let action;
1420
+ let reason;
1421
+ if (next.plans === 0) {
1422
+ action = `/gsd-discuss-phase ${n}`;
1423
+ reason = `Phase ${n} has no plans yet \u2014 start with discussion`;
1424
+ } else if (next.summaries < next.plans) {
1425
+ action = `/gsd-execute-phase ${n}`;
1426
+ reason = `Phase ${n}: ${next.summaries}/${next.plans} plans done \u2014 continue execution`;
1427
+ } else {
1428
+ action = `/gsd-verify-work ${n}`;
1429
+ reason = `Phase ${n}: all plans done \u2014 verify UAT`;
1430
+ }
1431
+ ctx.ui.notify(
1432
+ [
1433
+ `\u2501\u2501 GSD Next \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1434
+ `\u23E9 ${reason}`,
1435
+ `\u2192 ${action}`,
1436
+ ...pending.length > 1 ? [
1437
+ ` (${pending.length - 1} more phase${pending.length > 2 ? "s" : ""} pending after this)`
1438
+ ] : [],
1439
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`
1440
+ ].join("\n"),
1441
+ "info"
1442
+ );
1443
+ ctx.ui.setEditorText(action);
1444
+ }
1445
+ });
1446
+ pi.registerCommand("gsd-help", {
1447
+ description: "List all GSD commands (instant)",
1448
+ handler: async (_args, ctx) => {
1449
+ ctx.ui.notify(
1450
+ [
1451
+ "\u2501\u2501 GSD Commands \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
1452
+ "Lifecycle:",
1453
+ " /gsd-new-project Initialise project",
1454
+ " /gsd-new-milestone Start next milestone",
1455
+ " /gsd-discuss-phase N Discuss before planning",
1456
+ " /gsd-plan-phase N Create phase plan",
1457
+ " /gsd-execute-phase N Execute phase",
1458
+ " /gsd-verify-work N UAT testing",
1459
+ " /gsd-validate-phase N Validate completion",
1460
+ " /gsd-next Auto-advance",
1461
+ " /gsd-autonomous Run all phases",
1462
+ " /gsd-plan-milestone Plan all phases at once",
1463
+ " /gsd-execute-milestone Execute all phases with gates",
1464
+ "",
1465
+ "Quick:",
1466
+ " /gsd-quick <task> Tracked ad-hoc task",
1467
+ " /gsd-fast <task> Inline, no subagents",
1468
+ " /gsd-do <text> Route automatically",
1469
+ " /gsd-debug Debug session",
1470
+ "",
1471
+ "Instant (no LLM):",
1472
+ " /gsd-progress Progress + next steps",
1473
+ " /gsd-stats Full statistics",
1474
+ " /gsd-health [--repair] .planning/ integrity",
1475
+ " /gsd-help This list",
1476
+ "",
1477
+ "Management:",
1478
+ " /gsd-setup-pi Wire pi extension",
1479
+ " /gsd-set-profile <p> quality|balanced|budget",
1480
+ " /gsd-settings Workflow toggles",
1481
+ " /gsd-progress Roadmap overview",
1482
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"
1483
+ ].join("\n"),
1484
+ "info"
1485
+ );
1486
+ }
1487
+ });
1488
+ const WARNING_THRESHOLD = 35;
1489
+ const CRITICAL_THRESHOLD = 25;
1490
+ const DEBOUNCE_CALLS = 5;
1491
+ let callsSinceWarn = 0;
1492
+ let lastLevel = null;
1493
+ pi.on("tool_result", async (_event, ctx) => {
1494
+ try {
1495
+ const usage = ctx.getContextUsage();
1496
+ if (!usage || usage.percent === null) return void 0;
1497
+ const usedPct = Math.round(usage.percent);
1498
+ const remaining = 100 - usedPct;
1499
+ if (remaining > WARNING_THRESHOLD) {
1500
+ callsSinceWarn++;
1501
+ return void 0;
1502
+ }
1503
+ const configPath = (0, import_node_path4.join)(ctx.cwd, ".planning", "config.json");
1504
+ if ((0, import_node_fs3.existsSync)(configPath)) {
1505
+ try {
1506
+ const config = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
1507
+ if (config.hooks?.context_warnings === false) return void 0;
1508
+ } catch {
1509
+ }
1510
+ }
1511
+ const isCritical = remaining <= CRITICAL_THRESHOLD;
1512
+ const currentLevel = isCritical ? "critical" : "warning";
1513
+ callsSinceWarn++;
1514
+ const severityEscalated = currentLevel === "critical" && lastLevel === "warning";
1515
+ if (lastLevel !== null && callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
1516
+ return void 0;
1517
+ }
1518
+ callsSinceWarn = 0;
1519
+ lastLevel = currentLevel;
1520
+ const isGsdActive = (0, import_node_fs3.existsSync)((0, import_node_path4.join)(ctx.cwd, ".planning", "STATE.md"));
1521
+ let msg;
1522
+ if (isCritical) {
1523
+ msg = isGsdActive ? `\u{1F534} CONTEXT CRITICAL: ${usedPct}% used (${remaining}% left). GSD state is in STATE.md. Inform user to run /gsd-pause-work.` : `\u{1F534} CONTEXT CRITICAL: ${usedPct}% used (${remaining}% left). Inform user context is nearly exhausted.`;
1524
+ } else {
1525
+ msg = isGsdActive ? `\u26A0\uFE0F CONTEXT WARNING: ${usedPct}% used (${remaining}% left). Avoid starting new complex work.` : `\u26A0\uFE0F CONTEXT WARNING: ${usedPct}% used (${remaining}% left). Context is getting limited.`;
1526
+ }
1527
+ ctx.ui.notify(msg, isCritical ? "error" : "info");
1528
+ } catch {
1529
+ }
1530
+ return void 0;
1531
+ });
1532
+ }