pi-gsd 2.0.1 → 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.
Files changed (65) hide show
  1. package/dist/pi-gsd-hooks.js +1532 -0
  2. package/package.json +3 -5
  3. package/.gsd/extensions/pi-gsd-hooks.ts +0 -973
  4. package/src/cli.ts +0 -644
  5. package/src/commands/base.ts +0 -67
  6. package/src/commands/commit.ts +0 -22
  7. package/src/commands/config.ts +0 -71
  8. package/src/commands/frontmatter.ts +0 -51
  9. package/src/commands/index.ts +0 -76
  10. package/src/commands/init.ts +0 -43
  11. package/src/commands/milestone.ts +0 -37
  12. package/src/commands/phase.ts +0 -92
  13. package/src/commands/progress.ts +0 -71
  14. package/src/commands/roadmap.ts +0 -40
  15. package/src/commands/scaffold.ts +0 -19
  16. package/src/commands/state.ts +0 -102
  17. package/src/commands/template.ts +0 -52
  18. package/src/commands/verify.ts +0 -70
  19. package/src/commands/workstream.ts +0 -98
  20. package/src/commands/wxp.ts +0 -65
  21. package/src/lib/commands.ts +0 -1040
  22. package/src/lib/config.ts +0 -385
  23. package/src/lib/core.ts +0 -1167
  24. package/src/lib/frontmatter.ts +0 -462
  25. package/src/lib/init.ts +0 -517
  26. package/src/lib/milestone.ts +0 -290
  27. package/src/lib/model-profiles.ts +0 -272
  28. package/src/lib/phase.ts +0 -1012
  29. package/src/lib/profile-output.ts +0 -237
  30. package/src/lib/profile-pipeline.ts +0 -556
  31. package/src/lib/roadmap.ts +0 -378
  32. package/src/lib/schemas.ts +0 -290
  33. package/src/lib/security.ts +0 -176
  34. package/src/lib/state.ts +0 -1175
  35. package/src/lib/template.ts +0 -246
  36. package/src/lib/uat.ts +0 -289
  37. package/src/lib/verify.ts +0 -879
  38. package/src/lib/workstream.ts +0 -524
  39. package/src/output.ts +0 -45
  40. package/src/schemas/pi-gsd-settings.schema.json +0 -80
  41. package/src/schemas/wxp.xsd +0 -619
  42. package/src/schemas/wxp.zod.ts +0 -318
  43. package/src/wxp/__tests__/arguments.test.ts +0 -86
  44. package/src/wxp/__tests__/conditions.test.ts +0 -106
  45. package/src/wxp/__tests__/executor.test.ts +0 -95
  46. package/src/wxp/__tests__/helpers.ts +0 -26
  47. package/src/wxp/__tests__/integration.test.ts +0 -166
  48. package/src/wxp/__tests__/new-features.test.ts +0 -222
  49. package/src/wxp/__tests__/parser.test.ts +0 -159
  50. package/src/wxp/__tests__/paste.test.ts +0 -66
  51. package/src/wxp/__tests__/schema.test.ts +0 -120
  52. package/src/wxp/__tests__/security.test.ts +0 -87
  53. package/src/wxp/__tests__/shell.test.ts +0 -85
  54. package/src/wxp/__tests__/string-ops.test.ts +0 -25
  55. package/src/wxp/__tests__/variables.test.ts +0 -65
  56. package/src/wxp/arguments.ts +0 -89
  57. package/src/wxp/conditions.ts +0 -78
  58. package/src/wxp/executor.ts +0 -191
  59. package/src/wxp/index.ts +0 -191
  60. package/src/wxp/parser.ts +0 -198
  61. package/src/wxp/paste.ts +0 -51
  62. package/src/wxp/security.ts +0 -102
  63. package/src/wxp/shell.ts +0 -81
  64. package/src/wxp/string-ops.ts +0 -44
  65. package/src/wxp/variables.ts +0 -109
@@ -1,378 +0,0 @@
1
- /**
2
- * roadmap.ts - Roadmap parsing and update operations.
3
- */
4
-
5
- import fs from "fs";
6
- import path from "path";
7
- import {
8
- comparePhaseNum,
9
- escapeRegex,
10
- extractCurrentMilestone,
11
- findPhaseInternal,
12
- gsdError,
13
- loadConfig,
14
- normalizePhaseName,
15
- output,
16
- planningPaths,
17
- replaceInCurrentMilestone,
18
- stripShippedMilestones,
19
- } from "./core.js";
20
- import { extractFrontmatter } from "./frontmatter.js";
21
-
22
- export function cmdRoadmapGetPhase(
23
- cwd: string,
24
- phaseNum: string | undefined,
25
- raw: boolean,
26
- ): void {
27
- const roadmapPath = planningPaths(cwd).roadmap;
28
- if (!fs.existsSync(roadmapPath)) {
29
- output({ found: false, error: "ROADMAP.md not found" }, raw, "");
30
- return;
31
- }
32
- try {
33
- const content = extractCurrentMilestone(
34
- fs.readFileSync(roadmapPath, "utf-8"),
35
- cwd,
36
- );
37
- const escapedPhase = escapeRegex(phaseNum ?? "");
38
- const phasePattern = new RegExp(
39
- `#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
40
- "i",
41
- );
42
- const headerMatch = content.match(phasePattern);
43
- if (!headerMatch) {
44
- const checklistPattern = new RegExp(
45
- `-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${escapedPhase}:\\s*([^*]+)\\*\\*`,
46
- "i",
47
- );
48
- const checklistMatch = content.match(checklistPattern);
49
- if (checklistMatch) {
50
- output(
51
- {
52
- found: false,
53
- phase_number: phaseNum,
54
- phase_name: checklistMatch[1].trim(),
55
- error: "malformed_roadmap",
56
- message: `Phase ${phaseNum} exists in summary list but missing detail section.`,
57
- },
58
- raw,
59
- "",
60
- );
61
- return;
62
- }
63
- output({ found: false, phase_number: phaseNum }, raw, "");
64
- return;
65
- }
66
- const phaseName = headerMatch[1].trim();
67
- const headerIndex = headerMatch.index!;
68
- const restOfContent = content.slice(headerIndex);
69
- const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
70
- const sectionEnd = nextHeaderMatch
71
- ? headerIndex + nextHeaderMatch.index!
72
- : content.length;
73
- const section = content.slice(headerIndex, sectionEnd).trim();
74
- const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
75
- const criteriaMatch = section.match(
76
- /\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i,
77
- );
78
- const success_criteria = criteriaMatch
79
- ? criteriaMatch[1]
80
- .trim()
81
- .split("\n")
82
- .map((l) => l.replace(/^\s*\d+\.\s*/, "").trim())
83
- .filter(Boolean)
84
- : [];
85
- output(
86
- {
87
- found: true,
88
- phase_number: phaseNum,
89
- phase_name: phaseName,
90
- goal: goalMatch ? goalMatch[1].trim() : null,
91
- success_criteria,
92
- section,
93
- },
94
- raw,
95
- section,
96
- );
97
- } catch (e) {
98
- gsdError("Failed to read ROADMAP.md: " + (e as Error).message);
99
- }
100
- }
101
-
102
- interface RoadmapPhaseItem {
103
- number: string;
104
- name: string;
105
- goal: string | null;
106
- depends_on: string | null;
107
- plan_count: number;
108
- summary_count: number;
109
- has_context: boolean;
110
- has_research: boolean;
111
- disk_status: string;
112
- roadmap_complete: boolean;
113
- }
114
-
115
- export function cmdRoadmapAnalyze(cwd: string, raw: boolean): void {
116
- const roadmapPath = planningPaths(cwd).roadmap;
117
- if (!fs.existsSync(roadmapPath)) {
118
- output(
119
- {
120
- error: "ROADMAP.md not found",
121
- milestones: [],
122
- phases: [],
123
- current_phase: null,
124
- },
125
- raw,
126
- );
127
- return;
128
- }
129
- const rawContent = fs.readFileSync(roadmapPath, "utf-8");
130
- const content = extractCurrentMilestone(rawContent, cwd);
131
- const phasesDir = planningPaths(cwd).phases;
132
-
133
- const phasePattern =
134
- /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
135
- const phases: RoadmapPhaseItem[] = [];
136
- let match: RegExpExecArray | null;
137
-
138
- while ((match = phasePattern.exec(content)) !== null) {
139
- const phaseNum = match[1],
140
- phaseName = match[2].replace(/\(INSERTED\)/i, "").trim();
141
- const sectionStart = match.index;
142
- const nextHeader = content
143
- .slice(sectionStart)
144
- .match(/\n#{2,4}\s+Phase\s+\d/i);
145
- const sectionEnd = nextHeader
146
- ? sectionStart + nextHeader.index!
147
- : content.length;
148
- const section = content.slice(sectionStart, sectionEnd);
149
- const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
150
- const dependsMatch = section.match(
151
- /\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i,
152
- );
153
- const normalized = normalizePhaseName(phaseNum);
154
- let diskStatus = "no_directory",
155
- planCount = 0,
156
- summaryCount = 0,
157
- hasContext = false,
158
- hasResearch = false;
159
- try {
160
- const dirs = fs
161
- .readdirSync(phasesDir, { withFileTypes: true })
162
- .filter((e) => e.isDirectory())
163
- .map((e) => e.name);
164
- const dirMatch = dirs.find(
165
- (d) => d.startsWith(normalized + "-") || d === normalized,
166
- );
167
- if (dirMatch) {
168
- const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
169
- planCount = phaseFiles.filter(
170
- (f) => f.endsWith("-PLAN.md") || f === "PLAN.md",
171
- ).length;
172
- summaryCount = phaseFiles.filter(
173
- (f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md",
174
- ).length;
175
- hasContext = phaseFiles.some(
176
- (f) => f.endsWith("-CONTEXT.md") || f === "CONTEXT.md",
177
- );
178
- hasResearch = phaseFiles.some(
179
- (f) => f.endsWith("-RESEARCH.md") || f === "RESEARCH.md",
180
- );
181
- if (summaryCount >= planCount && planCount > 0) diskStatus = "complete";
182
- else if (summaryCount > 0) diskStatus = "partial";
183
- else if (planCount > 0) diskStatus = "planned";
184
- else if (hasResearch) diskStatus = "researched";
185
- else if (hasContext) diskStatus = "discussed";
186
- else diskStatus = "empty";
187
- }
188
- } catch {
189
- /* ok */
190
- }
191
- const checkboxPattern = new RegExp(
192
- `-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s]`,
193
- "i",
194
- );
195
- const checkboxMatch = content.match(checkboxPattern);
196
- const roadmapComplete = checkboxMatch ? checkboxMatch[1] === "x" : false;
197
- if (roadmapComplete && diskStatus !== "complete") diskStatus = "complete";
198
- phases.push({
199
- number: phaseNum,
200
- name: phaseName,
201
- goal: goalMatch ? goalMatch[1].trim() : null,
202
- depends_on: dependsMatch ? dependsMatch[1].trim() : null,
203
- plan_count: planCount,
204
- summary_count: summaryCount,
205
- has_context: hasContext,
206
- has_research: hasResearch,
207
- disk_status: diskStatus,
208
- roadmap_complete: roadmapComplete,
209
- });
210
- }
211
-
212
- const milestones: Array<{ heading: string; version: string }> = [];
213
- const milestonePattern = /##\s*(.*v(\d+(?:\.\d+)+)[^(\n]*)/gi;
214
- let mMatch: RegExpExecArray | null;
215
- while ((mMatch = milestonePattern.exec(content)) !== null)
216
- milestones.push({ heading: mMatch[1].trim(), version: "v" + mMatch[2] });
217
-
218
- const currentPhase =
219
- phases.find(
220
- (p) => p.disk_status === "planned" || p.disk_status === "partial",
221
- ) ?? null;
222
- const nextPhase =
223
- phases.find((p) =>
224
- ["empty", "no_directory", "discussed", "researched"].includes(
225
- p.disk_status,
226
- ),
227
- ) ?? null;
228
- const totalPlans = phases.reduce(
229
- (s: number, p: { plan_count: number }) => s + p.plan_count,
230
- 0,
231
- );
232
- const totalSummaries = phases.reduce(
233
- (s: number, p: { summary_count: number }) => s + p.summary_count,
234
- 0,
235
- );
236
- const completedPhases = phases.filter(
237
- (p: { disk_status: string }) => p.disk_status === "complete",
238
- ).length;
239
-
240
- const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi;
241
- const checklistPhases = new Set<string>();
242
- let clm: RegExpExecArray | null;
243
- while ((clm = checklistPattern.exec(content)) !== null)
244
- checklistPhases.add(clm[1]);
245
- const detailPhases = new Set(phases.map((p: { number: string }) => p.number));
246
- const missingDetails = [...checklistPhases].filter(
247
- (p) => !detailPhases.has(p),
248
- );
249
-
250
- output(
251
- {
252
- milestones,
253
- phases,
254
- phase_count: phases.length,
255
- completed_phases: completedPhases,
256
- total_plans: totalPlans,
257
- total_summaries: totalSummaries,
258
- progress_percent:
259
- totalPlans > 0
260
- ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100))
261
- : 0,
262
- current_phase: currentPhase ? currentPhase.number : null,
263
- next_phase: nextPhase ? nextPhase.number : null,
264
- missing_phase_details: missingDetails.length > 0 ? missingDetails : null,
265
- },
266
- raw,
267
- );
268
- }
269
-
270
- export function cmdRoadmapUpdatePlanProgress(
271
- cwd: string,
272
- phaseNum: string | undefined,
273
- raw: boolean,
274
- ): void {
275
- if (!phaseNum)
276
- gsdError("phase number required for roadmap update-plan-progress");
277
- const roadmapPath = planningPaths(cwd).roadmap;
278
- const phaseInfo = findPhaseInternal(cwd, phaseNum!);
279
- if (!phaseInfo) gsdError(`Phase ${phaseNum} not found`);
280
- const planCount = phaseInfo!.plans.length,
281
- summaryCount = phaseInfo!.summaries.length;
282
- if (planCount === 0) {
283
- output(
284
- {
285
- updated: false,
286
- reason: "No plans found",
287
- plan_count: 0,
288
- summary_count: 0,
289
- },
290
- raw,
291
- "no plans",
292
- );
293
- return;
294
- }
295
- const isComplete = summaryCount >= planCount;
296
- const status = isComplete
297
- ? "Complete"
298
- : summaryCount > 0
299
- ? "In Progress"
300
- : "Planned";
301
- const today = new Date().toISOString().split("T")[0];
302
- if (!fs.existsSync(roadmapPath)) {
303
- output(
304
- {
305
- updated: false,
306
- reason: "ROADMAP.md not found",
307
- plan_count: planCount,
308
- summary_count: summaryCount,
309
- },
310
- raw,
311
- "no roadmap",
312
- );
313
- return;
314
- }
315
- let roadmapContent = fs.readFileSync(roadmapPath, "utf-8");
316
- const phaseEscaped = escapeRegex(phaseNum!);
317
- const tableRowPattern = new RegExp(
318
- `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
319
- "im",
320
- );
321
- const dateField = isComplete ? ` ${today} ` : " ";
322
- roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
323
- const cells = fullRow.split("|").slice(1, -1);
324
- if (cells.length === 5) {
325
- cells[2] = ` ${summaryCount}/${planCount} `;
326
- cells[3] = ` ${status.padEnd(11)}`;
327
- cells[4] = dateField;
328
- } else if (cells.length === 4) {
329
- cells[1] = ` ${summaryCount}/${planCount} `;
330
- cells[2] = ` ${status.padEnd(11)}`;
331
- cells[3] = dateField;
332
- }
333
- return "|" + cells.join("|") + "|";
334
- });
335
- const planCountPattern = new RegExp(
336
- `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
337
- "i",
338
- );
339
- roadmapContent = replaceInCurrentMilestone(
340
- roadmapContent,
341
- planCountPattern,
342
- `$1${isComplete ? `${summaryCount}/${planCount} plans complete` : `${summaryCount}/${planCount} plans executed`}`,
343
- );
344
- if (isComplete) {
345
- const checkboxPattern = new RegExp(
346
- `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
347
- "i",
348
- );
349
- roadmapContent = replaceInCurrentMilestone(
350
- roadmapContent,
351
- checkboxPattern,
352
- `$1x$2 (completed ${today})`,
353
- );
354
- }
355
- for (const summaryFile of phaseInfo!.summaries) {
356
- const planId = summaryFile
357
- .replace("-SUMMARY.md", "")
358
- .replace("SUMMARY.md", "");
359
- if (!planId) continue;
360
- roadmapContent = roadmapContent.replace(
361
- new RegExp(`(-\\s*\\[) (\\]\\s*${escapeRegex(planId)})`, "i"),
362
- "$1x$2",
363
- );
364
- }
365
- fs.writeFileSync(roadmapPath, roadmapContent, "utf-8");
366
- output(
367
- {
368
- updated: true,
369
- phase: phaseNum,
370
- plan_count: planCount,
371
- summary_count: summaryCount,
372
- status,
373
- complete: isComplete,
374
- },
375
- raw,
376
- `${summaryCount}/${planCount} ${status}`,
377
- );
378
- }
@@ -1,290 +0,0 @@
1
- /**
2
- * schemas.ts - Zod schemas for all .planning/ file structures.
3
- *
4
- * Covers: STATE.md frontmatter, ROADMAP.md phase entries, PLAN.md frontmatter,
5
- * UAT.md checkpoint structure, and .planning/config.json.
6
- *
7
- * All schemas are permissive (use .passthrough()) to accept 100% of valid
8
- * GSD v1.30.0 .planning/ files without error, even if they carry extra fields.
9
- *
10
- * TypeScript types are exported via z.infer<>.
11
- */
12
-
13
- import { z } from "zod";
14
-
15
- // ─── STATE.md Frontmatter ─────────────────────────────────────────────────────
16
-
17
- /**
18
- * Schema for STATE.md frontmatter key-value fields.
19
- *
20
- * STATE.md uses a bespoke "key: value" format (not strict YAML), parsed via
21
- * stateExtractField(). All fields are optional strings - the file may contain
22
- * only a subset depending on project state.
23
- */
24
- export const StateFrontmatterSchema = z
25
- .object({
26
- /** Active milestone version (e.g. "1.0.0") */
27
- milestone: z.string().optional(),
28
- /** Human-readable milestone name */
29
- milestone_name: z.string().optional(),
30
- /** Current phase number (e.g. "3", "3.1") */
31
- current_phase: z.string().optional(),
32
- /** Descriptive name of the current phase */
33
- current_phase_name: z.string().optional(),
34
- /** Current plan filename (e.g. "001-PLAN.md") */
35
- current_plan: z.string().optional(),
36
- /** Total number of phases in the milestone */
37
- total_phases: z.coerce.number().int().nonnegative().optional(),
38
- /** Total number of plans within the current phase */
39
- total_plans_in_phase: z.coerce.number().int().nonnegative().optional(),
40
- /**
41
- * Workflow status - free-text field.
42
- * Common values: "In Progress", "Paused", "Stopped", "Complete".
43
- */
44
- status: z.string().optional(),
45
- /** Percentage completion (e.g. "42%") */
46
- progress: z.string().optional(),
47
- /** ISO-8601 timestamp or human-readable last-activity string */
48
- last_activity: z.string().optional(),
49
- /** ISO-8601 or human-readable timestamp when work was paused */
50
- paused_at: z.string().optional(),
51
- /** ISO-8601 or human-readable timestamp when work was stopped */
52
- stopped_at: z.string().optional(),
53
- })
54
- .passthrough();
55
-
56
- export type StateFrontmatter = z.infer<typeof StateFrontmatterSchema>;
57
-
58
- // ─── ROADMAP.md Phase Entry ───────────────────────────────────────────────────
59
-
60
- /**
61
- * Schema for a single parsed phase entry from ROADMAP.md.
62
- *
63
- * This represents the structured data returned by cmdRoadmapGetPhase() rather
64
- * than raw markdown - the roadmap file itself is free-form markdown.
65
- */
66
- export const RoadmapPhaseEntrySchema = z
67
- .object({
68
- /** Indicates a successful lookup */
69
- found: z.literal(true),
70
- /** Phase identifier, e.g. "3", "3.1", "10B" */
71
- phase_number: z.string(),
72
- /** Human-readable phase title */
73
- phase_name: z.string(),
74
- /** One-line goal statement extracted from **Goal:** line (null if absent) */
75
- goal: z.string().nullable(),
76
- /** Ordered success criteria items */
77
- success_criteria: z.array(z.string()).default([]),
78
- /** Raw markdown section text (for debugging/display) */
79
- section: z.string().optional(),
80
- })
81
- .passthrough();
82
-
83
- export type RoadmapPhaseEntry = z.infer<typeof RoadmapPhaseEntrySchema>;
84
-
85
- // ─── PLAN.md Frontmatter ──────────────────────────────────────────────────────
86
-
87
- /**
88
- * Schema for PLAN.md YAML frontmatter (written by /gsd-plan-phase).
89
- *
90
- * GSD v1.30.0 requires these 8 fields. Additional fields may be present
91
- * (handled by .passthrough()).
92
- */
93
- export const PlanFrontmatterSchema = z
94
- .object({
95
- /** Phase number this plan belongs to */
96
- phase: z.union([z.string(), z.number()]),
97
- /** Plan slug / short identifier (e.g. "001") */
98
- plan: z.union([z.string(), z.number()]),
99
- /**
100
- * Plan type.
101
- * Common values: "implementation", "research", "verification", "ui".
102
- */
103
- type: z.string(),
104
- /**
105
- * Execution wave for parallelization (e.g. 1, 2, "1").
106
- * Wave 1 plans may run in parallel; later waves depend on earlier ones.
107
- */
108
- wave: z.union([z.string(), z.number()]),
109
- /**
110
- * Dependency list - plan slugs that must complete before this plan runs.
111
- * Can be an empty string, a comma-separated list, or an array.
112
- */
113
- depends_on: z.union([z.string(), z.array(z.string())]),
114
- /**
115
- * Files this plan intends to create or modify.
116
- * Can be an empty string, comma-separated list, or array.
117
- */
118
- files_modified: z.union([z.string(), z.array(z.string())]),
119
- /** Whether this plan can run without human intervention */
120
- autonomous: z.union([z.boolean(), z.string()]),
121
- /**
122
- * Must-have deliverables / acceptance items.
123
- * Can be an empty string, a comma-separated list, or an array.
124
- */
125
- must_haves: z.union([z.string(), z.array(z.string())]),
126
- })
127
- .passthrough();
128
-
129
- export type PlanFrontmatter = z.infer<typeof PlanFrontmatterSchema>;
130
-
131
- /**
132
- * Schema for SUMMARY.md frontmatter (produced after phase execution).
133
- */
134
- export const SummaryFrontmatterSchema = z
135
- .object({
136
- /** Phase number (string or numeric) */
137
- phase: z.union([z.string(), z.number()]),
138
- /** Plan slug */
139
- plan: z.union([z.string(), z.number()]),
140
- /** Subsystem or area this plan affected */
141
- subsystem: z.string(),
142
- /** Comma-separated tags or array */
143
- tags: z.union([z.string(), z.array(z.string())]),
144
- /** Human-readable duration string (e.g. "45m", "2h") */
145
- duration: z.string(),
146
- /** ISO-8601 completion timestamp or human-readable date */
147
- completed: z.string(),
148
- })
149
- .passthrough();
150
-
151
- export type SummaryFrontmatter = z.infer<typeof SummaryFrontmatterSchema>;
152
-
153
- /**
154
- * Schema for VERIFICATION.md frontmatter.
155
- */
156
- export const VerificationFrontmatterSchema = z
157
- .object({
158
- /** Phase number */
159
- phase: z.union([z.string(), z.number()]),
160
- /** Whether verification passed */
161
- verified: z.union([z.boolean(), z.string()]),
162
- /** Verification status label (e.g. "pass", "fail", "partial") */
163
- status: z.string(),
164
- /** Numeric score (0–100) */
165
- score: z.union([z.number(), z.string()]),
166
- })
167
- .passthrough();
168
-
169
- export type VerificationFrontmatter = z.infer<
170
- typeof VerificationFrontmatterSchema
171
- >;
172
-
173
- // ─── UAT.md Checkpoint Structure ─────────────────────────────────────────────
174
-
175
- /**
176
- * Schema for a single UAT checkpoint item parsed from a -UAT.md file.
177
- *
178
- * Matches the UatItem interface used by cmdAuditUat().
179
- */
180
- export const UatCheckpointSchema = z
181
- .object({
182
- /** Optional numeric test index (1-based) */
183
- test: z.number().int().positive().optional(),
184
- /** Human-readable test name */
185
- name: z.string(),
186
- /** Expected outcome description */
187
- expected: z.string().optional(),
188
- /** Actual result ("pass", "fail", "blocked", "pending", etc.) */
189
- result: z.string(),
190
- /** Grouping category for this test */
191
- category: z.string(),
192
- /** Optional reason if test failed or is blocked */
193
- reason: z.string().optional(),
194
- /** Optional reference to blocking issue/phase */
195
- blocked_by: z.string().optional(),
196
- })
197
- .passthrough();
198
-
199
- export type UatCheckpoint = z.infer<typeof UatCheckpointSchema>;
200
-
201
- /**
202
- * Schema for the full structured UAT audit result (as returned by pi-gsd-tools).
203
- */
204
- export const UatAuditResultSchema = z
205
- .object({
206
- phase: z.string(),
207
- phase_dir: z.string(),
208
- file: z.string(),
209
- file_path: z.string(),
210
- type: z.literal("uat"),
211
- status: z.string(),
212
- items: z.array(UatCheckpointSchema),
213
- })
214
- .passthrough();
215
-
216
- export type UatAuditResult = z.infer<typeof UatAuditResultSchema>;
217
-
218
- // ─── .planning/config.json ────────────────────────────────────────────────────
219
-
220
- const GitConfigSchema = z
221
- .object({
222
- branching_strategy: z
223
- .enum(["none", "phase", "milestone", "workstream"])
224
- .default("none"),
225
- phase_branch_template: z.string().default("gsd/phase-{phase}-{slug}"),
226
- milestone_branch_template: z.string().default("gsd/{milestone}-{slug}"),
227
- quick_branch_template: z.string().nullable().default(null),
228
- })
229
- .passthrough();
230
-
231
- const WorkflowConfigSchema = z
232
- .object({
233
- research: z.boolean().default(true),
234
- plan_check: z.boolean().default(true),
235
- verifier: z.boolean().default(true),
236
- nyquist_validation: z.boolean().default(true),
237
- auto_advance: z.boolean().default(false),
238
- node_repair: z.boolean().default(true),
239
- node_repair_budget: z.number().int().nonnegative().default(2),
240
- auto_retry_audit: z.boolean().default(true),
241
- auto_retry_audit_budget: z.number().int().nonnegative().default(1),
242
- auto_retry_tech_debt: z.boolean().default(true),
243
- auto_retry_tech_debt_budget: z.number().int().nonnegative().default(1),
244
- ui_phase: z.boolean().default(true),
245
- ui_safety_gate: z.boolean().default(true),
246
- text_mode: z.boolean().default(false),
247
- research_before_questions: z.boolean().default(false),
248
- discuss_mode: z.string().default("discuss"),
249
- skip_discuss: z.boolean().default(false),
250
- _auto_chain_active: z.boolean().default(false),
251
- })
252
- .passthrough();
253
-
254
- const HooksConfigSchema = z
255
- .object({
256
- context_warnings: z.boolean().default(true),
257
- workflow_guard: z.boolean().default(false),
258
- })
259
- .passthrough();
260
-
261
- /**
262
- * Schema for .planning/config.json.
263
- *
264
- * Generated by /gsd-new-project (cmdConfigNewProject) and persisted for the
265
- * lifetime of the project. The outer object uses .passthrough() so that
266
- * user-added agent_skills entries and future GSD keys are accepted.
267
- */
268
- export const PlanningConfigSchema = z
269
- .object({
270
- model_profile: z
271
- .enum(["quality", "balanced", "budget", "inherit"])
272
- .default("balanced"),
273
- commit_docs: z.boolean().default(true),
274
- parallelization: z.boolean().default(true),
275
- search_gitignored: z.boolean().default(false),
276
- brave_search: z.boolean().default(false),
277
- firecrawl: z.boolean().default(false),
278
- exa_search: z.boolean().default(false),
279
- git: GitConfigSchema.default({}),
280
- workflow: WorkflowConfigSchema.default({}),
281
- hooks: HooksConfigSchema.default({}),
282
- /** Map of agent-type → enabled flag or config object */
283
- agent_skills: z.record(z.string(), z.unknown()).default({}),
284
- })
285
- .passthrough();
286
-
287
- export type PlanningConfig = z.infer<typeof PlanningConfigSchema>;
288
- export type GitConfig = z.infer<typeof GitConfigSchema>;
289
- export type WorkflowConfig = z.infer<typeof WorkflowConfigSchema>;
290
- export type HooksConfig = z.infer<typeof HooksConfigSchema>;