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.
- package/.gsd/extensions/pi-gsd-hooks.ts +330 -150
- package/.gsd/harnesses/pi/get-shit-done/workflows/add-phase.md +66 -11
- package/.gsd/harnesses/pi/get-shit-done/workflows/add-tests.md +69 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/add-todo.md +30 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/audit-milestone.md +75 -17
- package/.gsd/harnesses/pi/get-shit-done/workflows/audit-uat.md +38 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/autonomous.md +95 -286
- package/.gsd/harnesses/pi/get-shit-done/workflows/check-todos.md +67 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/cleanup.md +25 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/complete-milestone.md +51 -529
- package/.gsd/harnesses/pi/get-shit-done/workflows/diagnose-issues.md +39 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/discovery-phase.md +2 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/discuss-phase-assumptions.md +80 -5
- package/.gsd/harnesses/pi/get-shit-done/workflows/discuss-phase.md +43 -5
- package/.gsd/harnesses/pi/get-shit-done/workflows/discuss-phase.md.bak +1049 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/do.md +30 -3
- package/.gsd/harnesses/pi/get-shit-done/workflows/execute-milestone.md +64 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/execute-phase.md +78 -20
- package/.gsd/harnesses/pi/get-shit-done/workflows/execute-phase.md.bak +846 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/execute-plan.md +56 -19
- package/.gsd/harnesses/pi/get-shit-done/workflows/fast.md +2 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/forensics.md +40 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/health.md +25 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/help.md +2 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/insert-phase.md +69 -11
- package/.gsd/harnesses/pi/get-shit-done/workflows/list-phase-assumptions.md +2 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/list-workspaces.md +51 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/manager.md +81 -8
- package/.gsd/harnesses/pi/get-shit-done/workflows/map-codebase.md +40 -5
- package/.gsd/harnesses/pi/get-shit-done/workflows/milestone-summary.md +66 -48
- package/.gsd/harnesses/pi/get-shit-done/workflows/new-milestone.md +41 -13
- package/.gsd/harnesses/pi/get-shit-done/workflows/new-milestone.md.bak +486 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/new-project.md +43 -7
- package/.gsd/harnesses/pi/get-shit-done/workflows/new-project.md.bak +1250 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/new-workspace.md +55 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/next.md +39 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/node-repair.md +2 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/note.md +2 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/pause-work.md +46 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/plan-milestone-gaps.md +39 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/plan-milestone.md +40 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/plan-phase.md +57 -7
- package/.gsd/harnesses/pi/get-shit-done/workflows/plan-phase.md.bak +859 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/plant-seed.md +28 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/pr-branch.md +2 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/profile-user.md +51 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/progress.md +52 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/quick.md +99 -32
- package/.gsd/harnesses/pi/get-shit-done/workflows/remove-phase.md +66 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/remove-workspace.md +55 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/research-phase.md +79 -22
- package/.gsd/harnesses/pi/get-shit-done/workflows/resume-project.md +66 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/review.md +66 -36
- package/.gsd/harnesses/pi/get-shit-done/workflows/session-report.md +2 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/settings.md +27 -5
- package/.gsd/harnesses/pi/get-shit-done/workflows/ship.md +41 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/stats.md +24 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/transition.md +54 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/ui-phase.md +91 -6
- package/.gsd/harnesses/pi/get-shit-done/workflows/ui-review.md +80 -5
- package/.gsd/harnesses/pi/get-shit-done/workflows/update.md +2 -0
- package/.gsd/harnesses/pi/get-shit-done/workflows/validate-phase.md +90 -17
- package/.gsd/harnesses/pi/get-shit-done/workflows/verify-phase.md +79 -4
- package/.gsd/harnesses/pi/get-shit-done/workflows/verify-work.md +87 -31
- package/README.md +146 -112
- package/dist/pi-gsd-tools.js +166 -163
- package/package.json +13 -5
- package/prompts/gsd-add-backlog.md +2 -3
- package/prompts/gsd-add-phase.md +3 -2
- package/prompts/gsd-add-tests.md +3 -2
- package/prompts/gsd-add-todo.md +3 -2
- package/prompts/gsd-audit-milestone.md +3 -2
- package/prompts/gsd-audit-uat.md +3 -2
- package/prompts/gsd-autonomous.md +3 -2
- package/prompts/gsd-check-todos.md +3 -2
- package/prompts/gsd-cleanup.md +3 -2
- package/prompts/gsd-complete-milestone.md +2 -3
- package/prompts/gsd-debug.md +2 -3
- package/prompts/gsd-discuss-phase.md +4 -3
- package/prompts/gsd-do.md +3 -2
- package/prompts/gsd-execute-milestone.md +3 -2
- package/prompts/gsd-execute-phase.md +3 -2
- package/prompts/gsd-fast.md +2 -1
- package/prompts/gsd-forensics.md +3 -2
- package/prompts/gsd-insert-phase.md +3 -2
- package/prompts/gsd-join-discord.md +2 -3
- package/prompts/gsd-list-phase-assumptions.md +2 -1
- package/prompts/gsd-list-workspaces.md +3 -2
- package/prompts/gsd-manager.md +3 -2
- package/prompts/gsd-map-codebase.md +3 -2
- package/prompts/gsd-milestone-summary.md +3 -2
- package/prompts/gsd-new-milestone.md +3 -2
- package/prompts/gsd-new-project.md +3 -2
- package/prompts/gsd-new-workspace.md +3 -2
- package/prompts/gsd-note.md +2 -1
- package/prompts/gsd-pause-work.md +3 -2
- package/prompts/gsd-plan-milestone-gaps.md +3 -2
- package/prompts/gsd-plan-milestone.md +3 -2
- package/prompts/gsd-plan-phase.md +3 -2
- package/prompts/gsd-plant-seed.md +3 -2
- package/prompts/gsd-pr-branch.md +2 -1
- package/prompts/gsd-profile-user.md +3 -2
- package/prompts/gsd-quick.md +3 -2
- package/prompts/gsd-reapply-patches.md +2 -3
- package/prompts/gsd-remove-phase.md +3 -2
- package/prompts/gsd-remove-workspace.md +3 -2
- package/prompts/gsd-research-phase.md +2 -3
- package/prompts/gsd-resume-work.md +3 -2
- package/prompts/gsd-review-backlog.md +2 -3
- package/prompts/gsd-review.md +3 -2
- package/prompts/gsd-session-report.md +2 -1
- package/prompts/gsd-set-profile.md +2 -3
- package/prompts/gsd-settings.md +3 -2
- package/prompts/gsd-ship.md +3 -2
- package/prompts/gsd-thread.md +2 -3
- package/prompts/gsd-ui-phase.md +3 -2
- package/prompts/gsd-ui-review.md +3 -2
- package/prompts/gsd-validate-phase.md +3 -2
- package/prompts/gsd-verify-work.md +3 -2
- 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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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 {
|
|
353
|
+
return { messages };
|
|
213
354
|
});
|
|
214
355
|
|
|
215
356
|
// ── session_start: GSD update check ──────────────────────────────────────
|
|
216
357
|
pi.on("session_start", async (_event, ctx) => {
|
|
217
|
-
//
|
|
218
|
-
|
|
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
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
```
|