pi-gsd 1.12.3 → 2.0.0

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 (120) hide show
  1. package/.gsd/extensions/pi-gsd-hooks.ts +330 -150
  2. package/.gsd/harnesses/pi/get-shit-done/workflows/add-phase.md +66 -11
  3. package/.gsd/harnesses/pi/get-shit-done/workflows/add-tests.md +69 -4
  4. package/.gsd/harnesses/pi/get-shit-done/workflows/add-todo.md +30 -4
  5. package/.gsd/harnesses/pi/get-shit-done/workflows/audit-milestone.md +75 -17
  6. package/.gsd/harnesses/pi/get-shit-done/workflows/audit-uat.md +38 -0
  7. package/.gsd/harnesses/pi/get-shit-done/workflows/autonomous.md +95 -286
  8. package/.gsd/harnesses/pi/get-shit-done/workflows/check-todos.md +67 -4
  9. package/.gsd/harnesses/pi/get-shit-done/workflows/cleanup.md +25 -0
  10. package/.gsd/harnesses/pi/get-shit-done/workflows/complete-milestone.md +51 -529
  11. package/.gsd/harnesses/pi/get-shit-done/workflows/diagnose-issues.md +39 -0
  12. package/.gsd/harnesses/pi/get-shit-done/workflows/discovery-phase.md +2 -0
  13. package/.gsd/harnesses/pi/get-shit-done/workflows/discuss-phase-assumptions.md +80 -5
  14. package/.gsd/harnesses/pi/get-shit-done/workflows/discuss-phase.md +43 -5
  15. package/.gsd/harnesses/pi/get-shit-done/workflows/discuss-phase.md.bak +1049 -0
  16. package/.gsd/harnesses/pi/get-shit-done/workflows/do.md +30 -3
  17. package/.gsd/harnesses/pi/get-shit-done/workflows/execute-milestone.md +64 -0
  18. package/.gsd/harnesses/pi/get-shit-done/workflows/execute-phase.md +78 -20
  19. package/.gsd/harnesses/pi/get-shit-done/workflows/execute-phase.md.bak +846 -0
  20. package/.gsd/harnesses/pi/get-shit-done/workflows/execute-plan.md +56 -19
  21. package/.gsd/harnesses/pi/get-shit-done/workflows/fast.md +2 -0
  22. package/.gsd/harnesses/pi/get-shit-done/workflows/forensics.md +40 -0
  23. package/.gsd/harnesses/pi/get-shit-done/workflows/health.md +25 -0
  24. package/.gsd/harnesses/pi/get-shit-done/workflows/help.md +2 -0
  25. package/.gsd/harnesses/pi/get-shit-done/workflows/insert-phase.md +69 -11
  26. package/.gsd/harnesses/pi/get-shit-done/workflows/list-phase-assumptions.md +2 -0
  27. package/.gsd/harnesses/pi/get-shit-done/workflows/list-workspaces.md +51 -4
  28. package/.gsd/harnesses/pi/get-shit-done/workflows/manager.md +81 -8
  29. package/.gsd/harnesses/pi/get-shit-done/workflows/map-codebase.md +40 -5
  30. package/.gsd/harnesses/pi/get-shit-done/workflows/milestone-summary.md +66 -48
  31. package/.gsd/harnesses/pi/get-shit-done/workflows/new-milestone.md +41 -13
  32. package/.gsd/harnesses/pi/get-shit-done/workflows/new-milestone.md.bak +486 -0
  33. package/.gsd/harnesses/pi/get-shit-done/workflows/new-project.md +43 -7
  34. package/.gsd/harnesses/pi/get-shit-done/workflows/new-project.md.bak +1250 -0
  35. package/.gsd/harnesses/pi/get-shit-done/workflows/new-workspace.md +55 -4
  36. package/.gsd/harnesses/pi/get-shit-done/workflows/next.md +39 -0
  37. package/.gsd/harnesses/pi/get-shit-done/workflows/node-repair.md +2 -0
  38. package/.gsd/harnesses/pi/get-shit-done/workflows/note.md +2 -0
  39. package/.gsd/harnesses/pi/get-shit-done/workflows/pause-work.md +46 -0
  40. package/.gsd/harnesses/pi/get-shit-done/workflows/plan-milestone-gaps.md +39 -0
  41. package/.gsd/harnesses/pi/get-shit-done/workflows/plan-milestone.md +40 -0
  42. package/.gsd/harnesses/pi/get-shit-done/workflows/plan-phase.md +57 -7
  43. package/.gsd/harnesses/pi/get-shit-done/workflows/plan-phase.md.bak +859 -0
  44. package/.gsd/harnesses/pi/get-shit-done/workflows/plant-seed.md +28 -0
  45. package/.gsd/harnesses/pi/get-shit-done/workflows/pr-branch.md +2 -0
  46. package/.gsd/harnesses/pi/get-shit-done/workflows/profile-user.md +51 -0
  47. package/.gsd/harnesses/pi/get-shit-done/workflows/progress.md +52 -4
  48. package/.gsd/harnesses/pi/get-shit-done/workflows/quick.md +99 -32
  49. package/.gsd/harnesses/pi/get-shit-done/workflows/remove-phase.md +66 -4
  50. package/.gsd/harnesses/pi/get-shit-done/workflows/remove-workspace.md +55 -4
  51. package/.gsd/harnesses/pi/get-shit-done/workflows/research-phase.md +79 -22
  52. package/.gsd/harnesses/pi/get-shit-done/workflows/resume-project.md +66 -4
  53. package/.gsd/harnesses/pi/get-shit-done/workflows/review.md +66 -36
  54. package/.gsd/harnesses/pi/get-shit-done/workflows/session-report.md +2 -0
  55. package/.gsd/harnesses/pi/get-shit-done/workflows/settings.md +27 -5
  56. package/.gsd/harnesses/pi/get-shit-done/workflows/ship.md +41 -4
  57. package/.gsd/harnesses/pi/get-shit-done/workflows/stats.md +24 -0
  58. package/.gsd/harnesses/pi/get-shit-done/workflows/transition.md +54 -0
  59. package/.gsd/harnesses/pi/get-shit-done/workflows/ui-phase.md +91 -6
  60. package/.gsd/harnesses/pi/get-shit-done/workflows/ui-review.md +80 -5
  61. package/.gsd/harnesses/pi/get-shit-done/workflows/update.md +2 -0
  62. package/.gsd/harnesses/pi/get-shit-done/workflows/validate-phase.md +90 -17
  63. package/.gsd/harnesses/pi/get-shit-done/workflows/verify-phase.md +79 -4
  64. package/.gsd/harnesses/pi/get-shit-done/workflows/verify-work.md +87 -31
  65. package/README.md +146 -112
  66. package/dist/pi-gsd-tools.js +166 -163
  67. package/package.json +13 -5
  68. package/prompts/gsd-add-backlog.md +2 -3
  69. package/prompts/gsd-add-phase.md +3 -2
  70. package/prompts/gsd-add-tests.md +3 -2
  71. package/prompts/gsd-add-todo.md +3 -2
  72. package/prompts/gsd-audit-milestone.md +3 -2
  73. package/prompts/gsd-audit-uat.md +3 -2
  74. package/prompts/gsd-autonomous.md +3 -2
  75. package/prompts/gsd-check-todos.md +3 -2
  76. package/prompts/gsd-cleanup.md +3 -2
  77. package/prompts/gsd-complete-milestone.md +2 -3
  78. package/prompts/gsd-debug.md +2 -3
  79. package/prompts/gsd-discuss-phase.md +4 -3
  80. package/prompts/gsd-do.md +3 -2
  81. package/prompts/gsd-execute-milestone.md +3 -2
  82. package/prompts/gsd-execute-phase.md +3 -2
  83. package/prompts/gsd-fast.md +2 -1
  84. package/prompts/gsd-forensics.md +3 -2
  85. package/prompts/gsd-insert-phase.md +3 -2
  86. package/prompts/gsd-join-discord.md +2 -3
  87. package/prompts/gsd-list-phase-assumptions.md +2 -1
  88. package/prompts/gsd-list-workspaces.md +3 -2
  89. package/prompts/gsd-manager.md +3 -2
  90. package/prompts/gsd-map-codebase.md +3 -2
  91. package/prompts/gsd-milestone-summary.md +3 -2
  92. package/prompts/gsd-new-milestone.md +3 -2
  93. package/prompts/gsd-new-project.md +3 -2
  94. package/prompts/gsd-new-workspace.md +3 -2
  95. package/prompts/gsd-note.md +2 -1
  96. package/prompts/gsd-pause-work.md +3 -2
  97. package/prompts/gsd-plan-milestone-gaps.md +3 -2
  98. package/prompts/gsd-plan-milestone.md +3 -2
  99. package/prompts/gsd-plan-phase.md +3 -2
  100. package/prompts/gsd-plant-seed.md +3 -2
  101. package/prompts/gsd-pr-branch.md +2 -1
  102. package/prompts/gsd-profile-user.md +3 -2
  103. package/prompts/gsd-quick.md +3 -2
  104. package/prompts/gsd-reapply-patches.md +2 -3
  105. package/prompts/gsd-remove-phase.md +3 -2
  106. package/prompts/gsd-remove-workspace.md +3 -2
  107. package/prompts/gsd-research-phase.md +2 -3
  108. package/prompts/gsd-resume-work.md +3 -2
  109. package/prompts/gsd-review-backlog.md +2 -3
  110. package/prompts/gsd-review.md +3 -2
  111. package/prompts/gsd-session-report.md +2 -1
  112. package/prompts/gsd-set-profile.md +2 -3
  113. package/prompts/gsd-settings.md +3 -2
  114. package/prompts/gsd-ship.md +3 -2
  115. package/prompts/gsd-thread.md +2 -3
  116. package/prompts/gsd-ui-phase.md +3 -2
  117. package/prompts/gsd-ui-review.md +3 -2
  118. package/prompts/gsd-validate-phase.md +3 -2
  119. package/prompts/gsd-verify-work.md +3 -2
  120. package/prompts/gsd-workstreams.md +2 -3
@@ -18,18 +18,20 @@
18
18
 
19
19
  import { execSync } from "node:child_process";
20
20
  import {
21
+ copyFileSync,
21
22
  existsSync,
22
23
  lstatSync,
23
24
  mkdirSync,
24
25
  readFileSync,
25
- rmSync,
26
- statSync,
27
- symlinkSync,
26
+ readdirSync,
28
27
  writeFileSync,
29
28
  } from "node:fs";
30
29
  import { homedir } from "node:os";
31
- import { dirname, join } from "node:path";
30
+ import { dirname, join, relative } from "node:path";
32
31
  import type { ContextUsage, ExtensionAPI } from "@mariozechner/pi-coding-agent";
32
+ import { processWxpTrustedContent, WxpProcessingError, readWorkflowVersionTag } from "../../src/wxp/index.js";
33
+ import { DEFAULT_SHELL_ALLOWLIST } from "../../src/wxp/security.js";
34
+ import type { WxpSecurityConfig } from "../../src/schemas/wxp.zod.js";
33
35
 
34
36
  /**
35
37
  * Ensures .pi/gsd/ in the project is a symlink to the harness files
@@ -37,61 +39,196 @@ import type { ContextUsage, ExtensionAPI } from "@mariozechner/pi-coding-agent";
37
39
  * if already present. Never overwrites a real directory (user may have
38
40
  * customised it).
39
41
  */
40
- const ensureHarnessSymlink = (cwd: string): void => {
41
- try {
42
- const dest = join(cwd, ".pi", "gsd");
43
- // If dest exists, verify it's a valid symlink with files inside.
44
- // A stale real directory (from old build or worktree) must be replaced.
45
- if (existsSync(dest)) {
42
+
43
+ /**
44
+ * Copy-on-first-run harness distribution (HRN-01, HRN-03).
45
+ * - Detects symlinks and replaces with real file copies.
46
+ * - Copies missing files; never overwrites existing real files.
47
+ * - Silent on any failure (non-blocking).
48
+ */
49
+ function copyHarness(
50
+ src: string,
51
+ dest: string,
52
+ ): { symlinksReplaced: number; filesCopied: number } {
53
+ let symlinksReplaced = 0;
54
+ let filesCopied = 0;
55
+
56
+ const walk = (srcDir: string, destDir: string): void => {
57
+ mkdirSync(destDir, { recursive: true });
58
+ const entries = readdirSync(srcDir, { withFileTypes: true });
59
+ for (const entry of entries) {
60
+ const srcPath = join(srcDir, entry.name);
61
+ const destPath = join(destDir, entry.name);
62
+ if (entry.isDirectory()) {
63
+ walk(srcPath, destPath);
64
+ continue;
65
+ }
66
+ if (existsSync(destPath)) {
67
+ try {
68
+ const st = lstatSync(destPath);
69
+ if (st.isSymbolicLink()) {
70
+ // Replace symlink with real copy (HRN-03)
71
+ try {
72
+ // unlinkSync removes the symlink without following it
73
+ const { unlinkSync } = require("node:fs") as typeof import("node:fs");
74
+ unlinkSync(destPath);
75
+ } catch { /* ignore */ }
76
+ copyFileSync(srcPath, destPath);
77
+ symlinksReplaced++;
78
+ }
79
+ // Real file exists → skip (HRN-01: never overwrite)
80
+ } catch { /* ignore */ }
81
+ continue;
82
+ }
46
83
  try {
47
- const stat = statSync(dest);
48
- if (stat.isSymbolicLink?.() || lstatSync(dest).isSymbolicLink()) return; // valid symlink, done
49
- // Real directory check if it has the expected files
50
- if (existsSync(join(dest, "workflows", "execute-phase.md"))) return; // looks complete
51
- // Stale/incomplete directory — remove and replace with symlink
52
- rmSync(dest, { recursive: true, force: true });
53
- } catch {
54
- return; // can't inspect, leave it
84
+ copyFileSync(srcPath, destPath);
85
+ filesCopied++;
86
+ } catch { /* ignore */ }
87
+ }
88
+ };
89
+
90
+ walk(src, dest);
91
+ return { symlinksReplaced, filesCopied };
92
+ }
93
+
94
+ /**
95
+ * Extract the raw arguments string from a message that was produced by pi template expansion.
96
+ * Pi replaces $ARGUMENTS in prompt templates with the user's typed text.
97
+ * After <gsd-include> resolution, $ARGUMENTS text appears as trailing plain text
98
+ * at the end of the message — everything after the last WXP/include tag block.
99
+ *
100
+ * Example message after pi expansion + include resolution:
101
+ * [workflow content with <gsd-execute> blocks...]
102
+ * 16 --auto
103
+ *
104
+ * Returns: "16 --auto"
105
+ */
106
+ function extractRawArguments(content: string): string {
107
+ // Find the last <...> block (WXP tag or include) position
108
+ const lastTagEnd = (() => {
109
+ const tagPattern = /<\/(?:gsd-[a-zA-Z0-9_-]+|shell|if|then|else|condition|args|outs|string-op|settings)>/g;
110
+ let lastEnd = 0;
111
+ let m: RegExpExecArray | null;
112
+ while ((m = tagPattern.exec(content)) !== null) {
113
+ lastEnd = m.index + m[0].length;
114
+ }
115
+ return lastEnd;
116
+ })();
117
+
118
+ // Everything after the last closing tag is the trailing plain text ($ARGUMENTS expansion)
119
+ const trailing = content.slice(lastTagEnd).trim();
120
+
121
+ // Only return if it looks like user arguments (not a full document block)
122
+ // Reject if it contains markdown headings or is very long (probably included file content)
123
+ if (trailing.length === 0 || trailing.length > 500 || trailing.includes("\n\n\n")) {
124
+ return "";
125
+ }
126
+ return trailing;
127
+ }
128
+
129
+ export default function (pi: ExtensionAPI) {
130
+ /** Resolve a single <gsd-include> match: file lookup + selector extraction. */
131
+ function resolveGsdInclude(
132
+ match: RegExpMatchArray,
133
+ cwd: string,
134
+ pkgHarness: string,
135
+ errors: string[],
136
+ ): string | null {
137
+ const filePath = match[1];
138
+ const selectExpr = match[2] ?? "";
139
+
140
+ // ── Resolve file path ───────────────────────────────────────
141
+ const subPath = filePath.replace(/^\.pi\/gsd\//, "");
142
+ const candidates = [
143
+ join(cwd, filePath),
144
+ ...(filePath.startsWith(".pi/gsd/") && pkgHarness
145
+ ? [join(pkgHarness, subPath)]
146
+ : []),
147
+ ];
148
+
149
+ let raw: string | null = null;
150
+ for (const c of candidates) {
151
+ try {
152
+ if (existsSync(c)) {
153
+ raw = readFileSync(c, "utf8");
154
+ break;
55
155
  }
156
+ } catch {
157
+ /* try next */
56
158
  }
159
+ }
160
+ if (raw === null) {
161
+ errors.push("File not found: " + filePath);
162
+ return null;
163
+ }
57
164
 
58
- // Walk up from this extension file to the package root:
59
- // <pkg>/.gsd/extensions/pi-gsd-hooks.ts <pkg>
60
- const extFile = typeof __filename !== "undefined" ? __filename : "";
61
- const pkgRoot = join(dirname(extFile), "..", "..");
62
- const harnessSrc = join(
63
- pkgRoot,
64
- ".gsd",
65
- "harnesses",
66
- "pi",
67
- "get-shit-done",
68
- );
165
+ // ── Apply selector ─────────────────────────────────────────
166
+ let result = raw;
167
+ if (!selectExpr) return result;
69
168
 
70
- if (!existsSync(harnessSrc)) return; // package incomplete — skip silently
169
+ const parts = selectExpr.split("|");
170
+ if (parts.length > 2) {
171
+ errors.push("Invalid selector (max 2 segments): " + selectExpr);
172
+ return null;
173
+ }
174
+ if (parts.length > 1 && parts.some((p) => p.trim().startsWith("lines:"))) {
175
+ errors.push("lines: cannot be chained — use it alone: " + selectExpr);
176
+ return null;
177
+ }
178
+
179
+ for (const part of parts) {
180
+ const p = part.trim();
71
181
 
72
- mkdirSync(join(cwd, ".pi"), { recursive: true });
73
- symlinkSync(harnessSrc, dest, "dir");
74
- } catch {
75
- /* silent never block session startup */
182
+ if (p.startsWith("tag:")) {
183
+ const tagName = p.slice(4);
184
+ const tagRe = new RegExp("<" + tagName + ">([\\s\\S]*?)</" + tagName + ">", "i");
185
+ const tagMatch = result.match(tagRe);
186
+ if (!tagMatch) {
187
+ errors.push("Tag <" + tagName + "> not found in " + filePath);
188
+ return null;
189
+ }
190
+ result = tagMatch[1].trim();
191
+ } else if (p.startsWith("heading:")) {
192
+ const headingText = p.slice(8);
193
+ const escaped = headingText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
194
+ const headingRe = new RegExp("(^|\\n)(#{1,6})\\s+" + escaped + "\\s*\\n");
195
+ const hMatch = result.match(headingRe);
196
+ if (!hMatch) {
197
+ errors.push('Heading "' + headingText + '" not found in ' + filePath);
198
+ return null;
199
+ }
200
+ const level = hMatch[2].length;
201
+ const startIdx = (hMatch.index ?? 0) + hMatch[0].length;
202
+ const nextHeading = result.slice(startIdx).search(new RegExp("\\n#{1," + level + "}\\s"));
203
+ result =
204
+ nextHeading === -1
205
+ ? result.slice(startIdx).trim()
206
+ : result.slice(startIdx, startIdx + nextHeading).trim();
207
+ } else if (p.startsWith("lines:")) {
208
+ const rangeMatch = p.match(/^lines:(\d+)-(\d+)$/);
209
+ if (!rangeMatch) {
210
+ errors.push("Invalid lines selector: " + p);
211
+ return null;
212
+ }
213
+ const start = parseInt(rangeMatch[1], 10) - 1;
214
+ const end = parseInt(rangeMatch[2], 10);
215
+ result = result.split("\n").slice(start, end).join("\n");
216
+ } else {
217
+ errors.push("Unknown selector: " + p);
218
+ return null;
219
+ }
76
220
  }
77
- };
78
221
 
222
+ return result;
223
+ }
79
224
 
80
- export default function (pi: ExtensionAPI) {
81
- // ── input: <gsd-include> injection ───────────────────────────────────
82
- // Replaces <gsd-include path="..." /> tags with actual file contents.
83
- // Supports selectors: tag:NAME, heading:TEXT, lines:N-M
84
- // Valid chains: tag|heading, tag|lines, heading|lines, heading|tag
85
- // On ANY failure: red error + abort (action:"handled"). No partial injection.
86
- pi.on("input", async (event, ctx) => {
87
- if (event.source === "extension") return { action: "continue" };
88
-
89
- const text = event.text;
225
+ // ── context: <gsd-include> injection ────────────────────────────────────────
226
+ // Fires AFTER template expansion, before each LLM call.
227
+ // Scans user messages for <gsd-include path="..." select="..." /> tags,
228
+ // resolves files, applies selectors, replaces tags with content.
229
+ // On ANY failure: red error + return empty messages to block the LLM call.
230
+ pi.on("context", async (event, ctx) => {
90
231
  const includePattern = /<gsd-include\s+path="([^"]+)"(?:\s+select="([^"]*)")?\s*\/>/g;
91
- const includes = [...text.matchAll(includePattern)];
92
- ctx.ui.notify("[GSD] len=" + String(text?.length) + " inc=" + includes.length + " txt=[" + String(text).slice(0, 250) + "]", "info");
93
- if (includes.length === 0) return { action: "continue" };
94
-
95
232
 
96
233
  // Package harness fallback path
97
234
  const extFile = typeof __filename !== "undefined" ? __filename : "";
@@ -100,124 +237,167 @@ export default function (pi: ExtensionAPI) {
100
237
  : "";
101
238
 
102
239
  const errors: string[] = [];
103
- let transformed = text;
104
-
105
- for (const match of includes) {
106
- const fullMatch = match[0];
107
- const filePath = match[1];
108
- const selectExpr = match[2] ?? "";
109
-
110
- // ── Resolve file path ───────────────────────────────────────
111
- const subPath = filePath.replace(/^\.pi\/gsd\//, "");
112
- const candidates = [
113
- join(ctx.cwd, filePath),
114
- ...(filePath.startsWith(".pi/gsd/") && pkgHarness
115
- ? [join(pkgHarness, subPath)]
116
- : []),
117
- ];
240
+ const messages = event.messages;
118
241
 
119
- let raw: string | null = null;
120
- for (const c of candidates) {
121
- try {
122
- if (existsSync(c)) { raw = readFileSync(c, "utf8"); break; }
123
- } catch { /* try next */ }
124
- }
125
- if (raw === null) {
126
- errors.push(`File not found: ${filePath}`);
127
- continue;
128
- }
242
+ for (const msg of messages) {
243
+ if (msg.role !== "user") continue;
244
+
245
+ // Handle both string content and content block arrays
246
+ if (typeof msg.content === "string") {
247
+ const includes = [...msg.content.matchAll(includePattern)];
248
+ if (includes.length === 0) continue;
129
249
 
130
- // ── Apply selector ─────────────────────────────────────────
131
- let result = raw;
132
- if (selectExpr) {
133
- const parts = selectExpr.split("|");
134
- if (parts.length > 2) {
135
- errors.push(`Invalid selector (max 2 segments): ${selectExpr}`);
136
- continue;
250
+ let transformed = msg.content;
251
+ for (const match of includes) {
252
+ const replacement = resolveGsdInclude(match, ctx.cwd, pkgHarness, errors);
253
+ if (replacement === null) continue;
254
+ transformed = transformed.replace(match[0], replacement);
137
255
  }
138
- // lines: must be standalone — reject any chain involving lines
139
- if (parts.length > 1 && parts.some((p) => p.trim().startsWith("lines:"))) {
140
- errors.push(`lines: cannot be chained — use it alone: ${selectExpr}`);
141
- continue;
256
+ msg.content = transformed;
257
+ } else if (Array.isArray(msg.content)) {
258
+ for (const block of msg.content) {
259
+ if (block.type !== "text" || !block.text) continue;
260
+ const includes = [...block.text.matchAll(includePattern)];
261
+ if (includes.length === 0) continue;
262
+
263
+ let transformed = block.text;
264
+ for (const match of includes) {
265
+ const replacement = resolveGsdInclude(match, ctx.cwd, pkgHarness, errors);
266
+ if (replacement === null) continue;
267
+ transformed = transformed.replace(match[0], replacement);
268
+ }
269
+ block.text = transformed;
142
270
  }
271
+ }
272
+ }
143
273
 
144
- for (let i = 0; i < parts.length; i++) {
145
- const part = parts[i].trim();
146
- const prev = i > 0 ? parts[i - 1].trim().split(":")[0] : null;
274
+ if (errors.length > 0) {
275
+ ctx.ui.notify("\u274c GSD include failed:\n" + errors.map((e) => " \u2022 " + e).join("\n"), "error");
276
+ return { messages: [] }; // block LLM call
277
+ }
147
278
 
148
- if (part.startsWith("tag:")) {
149
- const tagName = part.slice(4);
150
- const tagRe = new RegExp(
151
- `<${tagName}>([\\s\\S]*?)</${tagName}>`,
152
- "i",
153
- );
154
- const tagMatch = result.match(tagRe);
155
- if (!tagMatch) {
156
- errors.push(`Tag <${tagName}> not found in ${filePath}`);
157
- result = "";
158
- break;
159
- }
160
- result = tagMatch[1].trim();
279
+ // ── WXP post-processing: run after <gsd-include> resolution (WXP-14) ──
280
+ // Load global + project settings (HRN-06, HRN-07)
281
+ const extFile2 = typeof __filename !== "undefined" ? __filename : "";
282
+ const pkgRoot2 = join(dirname(extFile2), "..", "..");
283
+
284
+ type SettingsFile = {
285
+ shellAllowlist?: string[];
286
+ shellBanlist?: string[];
287
+ trustedPaths?: Array<{ position: "project" | "pkg" | "absolute"; path: string }>;
288
+ untrustedPaths?: Array<{ position: "project" | "pkg" | "absolute"; path: string }>;
289
+ shellTimeoutMs?: number;
290
+ };
291
+ const loadSettings = (settingsPath: string): SettingsFile => {
292
+ try {
293
+ if (existsSync(settingsPath)) {
294
+ return JSON.parse(readFileSync(settingsPath, "utf8")) as SettingsFile;
295
+ }
296
+ } catch { /* ignore */ }
297
+ return {};
298
+ };
299
+ const globalSettings = loadSettings(join(homedir(), ".gsd", "pi-gsd-settings.json"));
300
+ const projectSettings = loadSettings(join(ctx.cwd, ".pi", "gsd", "pi-gsd-settings.json"));
301
+ const mergedAllowlist = [
302
+ ...DEFAULT_SHELL_ALLOWLIST,
303
+ ...(globalSettings.shellAllowlist ?? []),
304
+ ...(projectSettings.shellAllowlist ?? []),
305
+ ];
306
+ const wxpSecurity: WxpSecurityConfig = {
307
+ trustedPaths: [
308
+ ...(globalSettings.trustedPaths ?? []),
309
+ ...(projectSettings.trustedPaths ?? []),
310
+ { position: "pkg", path: ".gsd/harnesses/pi/get-shit-done" },
311
+ { position: "project", path: ".pi/gsd" },
312
+ ],
313
+ untrustedPaths: [
314
+ ...(globalSettings.untrustedPaths ?? []),
315
+ ...(projectSettings.untrustedPaths ?? []),
316
+ ],
317
+ shellAllowlist: [...new Set(mergedAllowlist)],
318
+ shellBanlist: [
319
+ ...(globalSettings.shellBanlist ?? []),
320
+ ...(projectSettings.shellBanlist ?? []),
321
+ ],
322
+ shellTimeoutMs: projectSettings.shellTimeoutMs ?? globalSettings.shellTimeoutMs ?? 30_000,
323
+ };
161
324
 
162
- } else if (part.startsWith("heading:")) {
163
- const headingText = part.slice(8);
164
- const headingRe = new RegExp(
165
- `(^|\\n)(#{1,6})\\s+${headingText.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}\\s*\\n`,
166
- );
167
- const hMatch = result.match(headingRe);
168
- if (!hMatch) {
169
- errors.push(`Heading \"${headingText}\" not found in ${filePath}`);
170
- result = "";
171
- break;
172
- }
173
- const level = hMatch[2].length;
174
- const startIdx = (hMatch.index ?? 0) + hMatch[0].length;
175
- const nextHeading = result.slice(startIdx).search(
176
- new RegExp(`\\n#{1,${level}}\\s`),
177
- );
178
- result = nextHeading === -1
179
- ? result.slice(startIdx).trim()
180
- : result.slice(startIdx, startIdx + nextHeading).trim();
181
-
182
- } else if (part.startsWith("lines:")) {
183
- const rangeMatch = part.match(/^lines:(\d+)-(\d+)$/);
184
- if (!rangeMatch) {
185
- errors.push(`Invalid lines selector: ${part}`);
186
- result = "";
187
- break;
188
- }
189
- const start = parseInt(rangeMatch[1], 10) - 1;
190
- const end = parseInt(rangeMatch[2], 10);
191
- result = result.split("\n").slice(start, end).join("\n");
192
-
193
- } else {
194
- errors.push(`Unknown selector: ${part}`);
195
- result = "";
196
- break;
325
+ try {
326
+ for (const msg of messages) {
327
+ if (msg.role !== "user") continue;
328
+ if (typeof msg.content === "string") {
329
+ if (!msg.content.includes("<gsd-")) continue;
330
+ const virtualPath = join(ctx.cwd, ".pi", "gsd", "workflows", "_message.md");
331
+ const rawArgs = extractRawArguments(msg.content);
332
+ msg.content = processWxpTrustedContent(msg.content, virtualPath, wxpSecurity, ctx.cwd, pkgRoot2, rawArgs, (m, lv) => ctx.ui.notify(m, lv === "error" ? "error" : "info"));
333
+ } else if (Array.isArray(msg.content)) {
334
+ for (const block of msg.content) {
335
+ if (block.type !== "text" || !block.text) continue;
336
+ if (!block.text.includes("<gsd-")) continue;
337
+ const virtualPath = join(ctx.cwd, ".pi", "gsd", "workflows", "_message.md");
338
+ const rawArgs = extractRawArguments(block.text);
339
+ block.text = processWxpTrustedContent(block.text, virtualPath, wxpSecurity, ctx.cwd, pkgRoot2, rawArgs, (m, lv) => ctx.ui.notify(m, lv === "error" ? "error" : "info"));
197
340
  }
198
341
  }
199
- if (result === "") continue; // error already logged
200
342
  }
201
-
202
- transformed = transformed.replace(fullMatch, result);
203
- }
204
-
205
- if (errors.length > 0) {
206
- ctx.ui.notify("\u274c GSD include failed:\n" + errors.map((e) => " \u2022 " + e).join("\n"),
207
- "error",
208
- );
209
- return { action: "handled" };
343
+ } catch (wxpErr) {
344
+ if (wxpErr instanceof WxpProcessingError) {
345
+ ctx.ui.notify(wxpErr.message, "error");
346
+ return { messages: [] }; // WXP-09: no partial content reaches LLM
347
+ }
348
+ // Non-WXP error: log but don't block
349
+ const errMsg = wxpErr instanceof Error ? wxpErr.message : String(wxpErr);
350
+ ctx.ui.notify(`GSD WXP: unexpected context error: ${errMsg}`, "info");
210
351
  }
211
352
 
212
- return { action: "transform", text: transformed };
353
+ return { messages };
213
354
  });
214
355
 
215
356
  // ── session_start: GSD update check ──────────────────────────────────────
216
357
  pi.on("session_start", async (_event, ctx) => {
217
- // Ensure harness files are reachable via .pi/gsd/ symlink
218
- ensureHarnessSymlink(ctx.cwd);
219
-
358
+ // Copy-on-first-run harness distribution (HRN-01, HRN-03)
359
+ try {
360
+ const extFile = typeof __filename !== "undefined" ? __filename : "";
361
+ const pkgRoot = join(dirname(extFile), "..", "..");
362
+ const pkgHarness = join(pkgRoot, ".gsd", "harnesses", "pi", "get-shit-done");
363
+ const projectHarness = join(ctx.cwd, ".pi", "gsd");
364
+ if (existsSync(pkgHarness)) {
365
+ const { symlinksReplaced } = copyHarness(pkgHarness, projectHarness);
366
+ if (symlinksReplaced > 0) {
367
+ ctx.ui.notify(
368
+ `ℹ️ GSD: Replaced ${symlinksReplaced} symlink(s) in .pi/gsd/ with real file copies.`,
369
+ "info",
370
+ );
371
+ }
220
372
 
373
+ // Version-aware update detection (HRN-02)
374
+ try {
375
+ const pkgJsonPath = join(pkgRoot, "package.json");
376
+ if (existsSync(pkgJsonPath)) {
377
+ const pkgVersion = (JSON.parse(readFileSync(pkgJsonPath, "utf8")) as { version?: string }).version ?? "0.0.0";
378
+ const outdated: string[] = [];
379
+ // Check a sample of key workflow files for version drift
380
+ const sampleFiles = ["workflows/execute-phase.md", "workflows/plan-phase.md"];
381
+ for (const rel of sampleFiles) {
382
+ const projFile = join(projectHarness, rel);
383
+ if (!existsSync(projFile)) continue;
384
+ const content = readFileSync(projFile, "utf8");
385
+ const vtag = readWorkflowVersionTag(content);
386
+ if (!vtag || vtag.doNotUpdate) continue;
387
+ if (vtag.version !== pkgVersion) outdated.push(rel);
388
+ }
389
+ if (outdated.length > 0) {
390
+ ctx.ui.notify(
391
+ `ℹ️ GSD harness update available (package v${pkgVersion}).\n` +
392
+ `Outdated files: ${outdated.join(", ")}\n` +
393
+ `Run: pi-gsd-tools harness update [y|n|pick|diff]`,
394
+ "info",
395
+ );
396
+ }
397
+ }
398
+ } catch { /* silent */ }
399
+ }
400
+ } catch { /* silent */ }
221
401
  try {
222
402
  const cacheDir = join(homedir(), ".pi", "cache");
223
403
  const cacheFile = join(cacheDir, "gsd-update-check.json");
@@ -1,10 +1,68 @@
1
- <purpose>
2
- Add a new integer phase to the end of the current milestone in the roadmap. Automatically calculates next phase number, creates phase directory, and updates roadmap structure.
3
- </purpose>
4
-
5
- <required_reading>
6
- Read all files referenced by the invoking prompt's execution_context before starting.
7
- </required_reading>
1
+ <gsd-version v="1.12.4" />
2
+
3
+ <gsd-arguments>
4
+ <settings>
5
+ <keep-extra-args />
6
+ </settings>
7
+ <arg name="description" type="string" optional />
8
+ </gsd-arguments>
9
+
10
+ <gsd-execute>
11
+ <shell command="pi-gsd-tools">
12
+ <args>
13
+ <arg string="init" />
14
+ <arg string="phase-op" />
15
+ <arg string="0" />
16
+ </args>
17
+ <outs>
18
+ <out type="string" name="init" />
19
+ </outs>
20
+ </shell>
21
+ <if>
22
+ <condition>
23
+ <starts-with>
24
+ <left name="init" />
25
+ <right type="string" value="@file:" />
26
+ </starts-with>
27
+ </condition>
28
+ <then>
29
+ <string-op op="split">
30
+ <args>
31
+ <arg name="init" />
32
+ <arg type="string" value="@file:" />
33
+ </args>
34
+ <outs>
35
+ <out type="string" name="init-file" />
36
+ </outs>
37
+ </string-op>
38
+ <shell command="cat">
39
+ <args>
40
+ <arg name="init-file" wrap='"' />
41
+ </args>
42
+ <outs>
43
+ <out type="string" name="init" />
44
+ </outs>
45
+ </shell>
46
+ </then>
47
+ </if>
48
+ <shell command="pi-gsd-tools">
49
+ <args>
50
+ <arg string="state" />
51
+ <arg string="json" />
52
+ <arg string="--raw" />
53
+ </args>
54
+ <outs>
55
+ <out type="string" name="state" />
56
+ </outs>
57
+ </shell>
58
+ </gsd-execute>
59
+
60
+ ## Context (pre-injected by WXP)
61
+
62
+ **Description:** <gsd-paste name="description" />
63
+
64
+ **Project State:**
65
+ <gsd-paste name="state" />
8
66
 
9
67
  <process>
10
68
 
@@ -28,10 +86,7 @@ Exit.
28
86
  <step name="init_context">
29
87
  Load phase operation context:
30
88
 
31
- ```bash
32
- INIT=$(pi-gsd-tools init phase-op "0")
33
- if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
34
- ```
89
+ <!-- Context pre-injected above via WXP — variables available via <gsd-paste name="..."> -->
35
90
 
36
91
  Check `roadmap_exists` from init JSON. If false:
37
92
  ```