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,524 +0,0 @@
1
- /**
2
- * workstream.ts - CRUD operations for workstream namespacing.
3
- */
4
-
5
- import fs from "fs";
6
- import path from "path";
7
- import {
8
- filterPlanFiles,
9
- filterSummaryFiles,
10
- generateSlugInternal,
11
- getActiveWorkstream,
12
- getMilestoneInfo,
13
- gsdError,
14
- output,
15
- planningPaths,
16
- planningRoot,
17
- readSubdirectories,
18
- setActiveWorkstream,
19
- toPosixPath,
20
- } from "./core.js";
21
- import { stateExtractField } from "./state.js";
22
-
23
- // ─── Migration ────────────────────────────────────────────────────────────────
24
-
25
- function migrateToWorkstreams(
26
- cwd: string,
27
- workstreamName: string,
28
- ): { migrated: boolean; workstream: string; files_moved: string[] } {
29
- if (
30
- !workstreamName ||
31
- /[/\\]/.test(workstreamName) ||
32
- workstreamName === "." ||
33
- workstreamName === ".."
34
- )
35
- throw new Error("Invalid workstream name for migration");
36
- const baseDir = planningRoot(cwd);
37
- const wsDir = path.join(baseDir, "workstreams", workstreamName);
38
- if (fs.existsSync(path.join(baseDir, "workstreams")))
39
- throw new Error(
40
- "Already in workstream mode - .planning/workstreams/ exists",
41
- );
42
- const toMove = [
43
- { name: "ROADMAP.md", type: "file" },
44
- { name: "STATE.md", type: "file" },
45
- { name: "REQUIREMENTS.md", type: "file" },
46
- { name: "phases", type: "dir" },
47
- ];
48
- fs.mkdirSync(wsDir, { recursive: true });
49
- const filesMoved: string[] = [];
50
- try {
51
- for (const item of toMove) {
52
- const src = path.join(baseDir, item.name);
53
- if (fs.existsSync(src)) {
54
- fs.renameSync(src, path.join(wsDir, item.name));
55
- filesMoved.push(item.name);
56
- }
57
- }
58
- } catch (err) {
59
- for (const name of filesMoved) {
60
- try {
61
- fs.renameSync(path.join(wsDir, name), path.join(baseDir, name));
62
- } catch {
63
- /* ok */
64
- }
65
- }
66
- try {
67
- fs.rmSync(wsDir, { recursive: true });
68
- } catch {
69
- /* ok */
70
- }
71
- try {
72
- fs.rmdirSync(path.join(baseDir, "workstreams"));
73
- } catch {
74
- /* ok */
75
- }
76
- throw err;
77
- }
78
- return {
79
- migrated: true,
80
- workstream: workstreamName,
81
- files_moved: filesMoved,
82
- };
83
- }
84
-
85
- // ─── Commands ─────────────────────────────────────────────────────────────────
86
-
87
- export function cmdWorkstreamCreate(
88
- cwd: string,
89
- name: string | undefined,
90
- options: { migrate?: boolean; migrateName?: string | null },
91
- raw: boolean,
92
- ): void {
93
- if (!name)
94
- gsdError("workstream name required. Usage: workstream create <name>");
95
- const slug = name!
96
- .toLowerCase()
97
- .replace(/[^a-z0-9]+/g, "-")
98
- .replace(/^-+|-+$/g, "");
99
- if (!slug)
100
- gsdError(
101
- "Invalid workstream name - must contain at least one alphanumeric character",
102
- );
103
- const baseDir = planningRoot(cwd);
104
- if (!fs.existsSync(baseDir))
105
- gsdError(".planning/ directory not found - run /gsd-new-project first");
106
- const wsRoot = path.join(baseDir, "workstreams"),
107
- wsDir = path.join(wsRoot, slug);
108
- if (fs.existsSync(wsDir) && fs.existsSync(path.join(wsDir, "STATE.md"))) {
109
- output(
110
- {
111
- created: false,
112
- error: "already_exists",
113
- workstream: slug,
114
- path: toPosixPath(path.relative(cwd, wsDir)),
115
- },
116
- raw,
117
- );
118
- return;
119
- }
120
- const isFlatMode = !fs.existsSync(wsRoot);
121
- let migration = null;
122
- if (isFlatMode && options.migrate !== false) {
123
- const hasExistingWork =
124
- fs.existsSync(path.join(baseDir, "ROADMAP.md")) ||
125
- fs.existsSync(path.join(baseDir, "STATE.md")) ||
126
- fs.existsSync(path.join(baseDir, "phases"));
127
- if (hasExistingWork) {
128
- const migrateName = options.migrateName ?? null;
129
- let existingWsName: string;
130
- if (migrateName) {
131
- existingWsName = migrateName;
132
- } else {
133
- try {
134
- const ms = getMilestoneInfo(cwd);
135
- existingWsName = generateSlugInternal(ms.name) || "default";
136
- } catch {
137
- existingWsName = "default";
138
- }
139
- }
140
- try {
141
- migration = migrateToWorkstreams(cwd, existingWsName);
142
- } catch (e) {
143
- output(
144
- {
145
- created: false,
146
- error: "migration_failed",
147
- message: (e as Error).message,
148
- },
149
- raw,
150
- );
151
- return;
152
- }
153
- } else {
154
- fs.mkdirSync(wsRoot, { recursive: true });
155
- }
156
- }
157
- fs.mkdirSync(wsDir, { recursive: true });
158
- fs.mkdirSync(path.join(wsDir, "phases"), { recursive: true });
159
- const today = new Date().toISOString().split("T")[0];
160
- const stateContent = [
161
- "---",
162
- `workstream: ${slug}`,
163
- `created: ${today}`,
164
- "---",
165
- "",
166
- "# Project State",
167
- "",
168
- "## Current Position",
169
- "**Status:** Not started",
170
- "**Current Phase:** None",
171
- `**Last Activity:** ${today}`,
172
- "**Last Activity Description:** Workstream created",
173
- "",
174
- "## Progress",
175
- "**Phases Complete:** 0",
176
- "**Current Plan:** N/A",
177
- "",
178
- "## Session Continuity",
179
- "**Stopped At:** N/A",
180
- "**Resume File:** None",
181
- "",
182
- ].join("\n");
183
- const statePath = path.join(wsDir, "STATE.md");
184
- if (!fs.existsSync(statePath))
185
- fs.writeFileSync(statePath, stateContent, "utf-8");
186
- setActiveWorkstream(cwd, slug);
187
- const relPath = toPosixPath(path.relative(cwd, wsDir));
188
- output(
189
- {
190
- created: true,
191
- workstream: slug,
192
- path: relPath,
193
- state_path: relPath + "/STATE.md",
194
- phases_path: relPath + "/phases",
195
- migration: migration ?? null,
196
- active: true,
197
- },
198
- raw,
199
- );
200
- }
201
-
202
- export function cmdWorkstreamList(cwd: string, raw: boolean): void {
203
- const wsRoot = path.join(planningRoot(cwd), "workstreams");
204
- if (!fs.existsSync(wsRoot)) {
205
- output(
206
- {
207
- mode: "flat",
208
- workstreams: [],
209
- message: "No workstreams - operating in flat mode",
210
- },
211
- raw,
212
- );
213
- return;
214
- }
215
- const workstreams: unknown[] = [];
216
- for (const entry of fs
217
- .readdirSync(wsRoot, { withFileTypes: true })
218
- .filter((e) => e.isDirectory())) {
219
- const wsDir = path.join(wsRoot, entry.name),
220
- phasesDir = path.join(wsDir, "phases");
221
- const phaseDirs = readSubdirectories(phasesDir);
222
- let completedCount = 0;
223
- for (const d of phaseDirs) {
224
- try {
225
- const fs2 = fs.readdirSync(path.join(phasesDir, d));
226
- const pl = filterPlanFiles(fs2),
227
- su = filterSummaryFiles(fs2);
228
- if (pl.length > 0 && su.length >= pl.length) completedCount++;
229
- } catch {
230
- /* ok */
231
- }
232
- }
233
- let status = "unknown",
234
- currentPhase = null;
235
- try {
236
- const sc = fs.readFileSync(path.join(wsDir, "STATE.md"), "utf-8");
237
- status = stateExtractField(sc, "Status") || "unknown";
238
- currentPhase = stateExtractField(sc, "Current Phase");
239
- } catch {
240
- /* ok */
241
- }
242
- workstreams.push({
243
- name: entry.name,
244
- path: toPosixPath(path.relative(cwd, wsDir)),
245
- has_roadmap: fs.existsSync(path.join(wsDir, "ROADMAP.md")),
246
- has_state: fs.existsSync(path.join(wsDir, "STATE.md")),
247
- status,
248
- current_phase: currentPhase,
249
- phase_count: phaseDirs.length,
250
- completed_phases: completedCount,
251
- });
252
- }
253
- output({ mode: "workstream", workstreams, count: workstreams.length }, raw);
254
- }
255
-
256
- export function cmdWorkstreamStatus(
257
- cwd: string,
258
- name: string | undefined,
259
- raw: boolean,
260
- ): void {
261
- if (!name)
262
- gsdError("workstream name required. Usage: workstream status <name>");
263
- if (/[/\\]/.test(name!) || name === "." || name === "..")
264
- gsdError("Invalid workstream name");
265
- const wsDir = path.join(planningRoot(cwd), "workstreams", name!);
266
- if (!fs.existsSync(wsDir)) {
267
- output({ found: false, workstream: name }, raw);
268
- return;
269
- }
270
- const p = planningPaths(cwd, name);
271
- const files = {
272
- roadmap: fs.existsSync(p.roadmap),
273
- state: fs.existsSync(p.state),
274
- requirements: fs.existsSync(p.requirements),
275
- };
276
- const phases: unknown[] = [];
277
- for (const dir of readSubdirectories(p.phases).sort()) {
278
- try {
279
- const pf = fs.readdirSync(path.join(p.phases, dir));
280
- const pl = filterPlanFiles(pf),
281
- su = filterSummaryFiles(pf);
282
- phases.push({
283
- directory: dir,
284
- status:
285
- su.length >= pl.length && pl.length > 0
286
- ? "complete"
287
- : pl.length > 0
288
- ? "in_progress"
289
- : "pending",
290
- plan_count: pl.length,
291
- summary_count: su.length,
292
- });
293
- } catch {
294
- /* ok */
295
- }
296
- }
297
- interface WorkstreamStateInfo { status: string; current_phase: string | null; last_activity: string | null; }
298
- let stateInfo: WorkstreamStateInfo = { status: "unknown", current_phase: null, last_activity: null };
299
- try {
300
- const sc = fs.readFileSync(p.state, "utf-8");
301
- stateInfo = {
302
- status: stateExtractField(sc, "Status") || "unknown",
303
- current_phase: stateExtractField(sc, "Current Phase"),
304
- last_activity: stateExtractField(sc, "Last Activity"),
305
- };
306
- } catch {
307
- /* ok */
308
- }
309
- output(
310
- {
311
- found: true,
312
- workstream: name,
313
- path: toPosixPath(path.relative(cwd, wsDir)),
314
- files,
315
- phases,
316
- phase_count: phases.length,
317
- completed_phases: (phases as { status: string }[]).filter(
318
- (ph) => ph.status === "complete",
319
- ).length,
320
- ...stateInfo,
321
- },
322
- raw,
323
- );
324
- }
325
-
326
- export function cmdWorkstreamComplete(
327
- cwd: string,
328
- name: string | undefined,
329
- options: Record<string, unknown>,
330
- raw: boolean,
331
- ): void {
332
- if (!name)
333
- gsdError("workstream name required. Usage: workstream complete <name>");
334
- if (/[/\\]/.test(name!) || name === "." || name === "..")
335
- gsdError("Invalid workstream name");
336
- const root = planningRoot(cwd),
337
- wsRoot = path.join(root, "workstreams"),
338
- wsDir = path.join(wsRoot, name!);
339
- if (!fs.existsSync(wsDir)) {
340
- output({ completed: false, error: "not_found", workstream: name }, raw);
341
- return;
342
- }
343
- const active = getActiveWorkstream(cwd);
344
- if (active === name) setActiveWorkstream(cwd, null);
345
- const archiveDir = path.join(root, "milestones");
346
- const today = new Date().toISOString().split("T")[0];
347
- let archivePath = path.join(archiveDir, `ws-${name}-${today}`);
348
- let suffix = 1;
349
- while (fs.existsSync(archivePath))
350
- archivePath = path.join(archiveDir, `ws-${name}-${today}-${suffix++}`);
351
- fs.mkdirSync(archivePath, { recursive: true });
352
- const filesMoved: string[] = [];
353
- try {
354
- for (const entry of fs.readdirSync(wsDir, { withFileTypes: true })) {
355
- fs.renameSync(
356
- path.join(wsDir, entry.name),
357
- path.join(archivePath, entry.name),
358
- );
359
- filesMoved.push(entry.name);
360
- }
361
- } catch (err) {
362
- for (const fname of filesMoved) {
363
- try {
364
- fs.renameSync(path.join(archivePath, fname), path.join(wsDir, fname));
365
- } catch {
366
- /* ok */
367
- }
368
- }
369
- try {
370
- fs.rmSync(archivePath, { recursive: true });
371
- } catch {
372
- /* ok */
373
- }
374
- if (active === name) setActiveWorkstream(cwd, name);
375
- output(
376
- {
377
- completed: false,
378
- error: "archive_failed",
379
- message: (err as Error).message,
380
- workstream: name,
381
- },
382
- raw,
383
- );
384
- return;
385
- }
386
- try {
387
- fs.rmdirSync(wsDir);
388
- } catch {
389
- /* ok */
390
- }
391
- let remainingWs = 0;
392
- try {
393
- remainingWs = fs
394
- .readdirSync(wsRoot, { withFileTypes: true })
395
- .filter((e) => e.isDirectory()).length;
396
- if (remainingWs === 0) fs.rmdirSync(wsRoot);
397
- } catch {
398
- /* ok */
399
- }
400
- output(
401
- {
402
- completed: true,
403
- workstream: name,
404
- archived_to: toPosixPath(path.relative(cwd, archivePath)),
405
- remaining_workstreams: remainingWs,
406
- reverted_to_flat: remainingWs === 0,
407
- },
408
- raw,
409
- );
410
- }
411
-
412
- export function cmdWorkstreamSet(
413
- cwd: string,
414
- name: string | undefined,
415
- raw: boolean,
416
- ): void {
417
- if (!name) {
418
- setActiveWorkstream(cwd, null);
419
- output({ active: null, cleared: true }, raw);
420
- return;
421
- }
422
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
423
- output(
424
- {
425
- active: null,
426
- error: "invalid_name",
427
- message:
428
- "Workstream name must be alphanumeric, hyphens, and underscores only",
429
- },
430
- raw,
431
- );
432
- return;
433
- }
434
- const wsDir = path.join(planningRoot(cwd), "workstreams", name);
435
- if (!fs.existsSync(wsDir)) {
436
- output({ active: null, error: "not_found", workstream: name }, raw);
437
- return;
438
- }
439
- setActiveWorkstream(cwd, name);
440
- output({ active: name, set: true }, raw, name);
441
- }
442
-
443
- export function cmdWorkstreamGet(cwd: string, raw: boolean): void {
444
- const active = getActiveWorkstream(cwd);
445
- const wsRoot = path.join(planningRoot(cwd), "workstreams");
446
- output(
447
- { active, mode: fs.existsSync(wsRoot) ? "workstream" : "flat" },
448
- raw,
449
- active || "none",
450
- );
451
- }
452
-
453
- export function cmdWorkstreamProgress(cwd: string, raw: boolean): void {
454
- const root = planningRoot(cwd),
455
- wsRoot = path.join(root, "workstreams");
456
- if (!fs.existsSync(wsRoot)) {
457
- output(
458
- {
459
- mode: "flat",
460
- workstreams: [],
461
- message: "No workstreams - operating in flat mode",
462
- },
463
- raw,
464
- );
465
- return;
466
- }
467
- const active = getActiveWorkstream(cwd);
468
- const workstreams: unknown[] = [];
469
- for (const entry of fs
470
- .readdirSync(wsRoot, { withFileTypes: true })
471
- .filter((e) => e.isDirectory())) {
472
- const wsDir = path.join(wsRoot, entry.name),
473
- phasesDir = path.join(wsDir, "phases");
474
- const phaseDirs = readSubdirectories(phasesDir);
475
- let completedCount = 0,
476
- totalPlans = 0,
477
- completedPlans = 0;
478
- for (const d of phaseDirs) {
479
- try {
480
- const pf = fs.readdirSync(path.join(phasesDir, d));
481
- const pl = filterPlanFiles(pf),
482
- su = filterSummaryFiles(pf);
483
- totalPlans += pl.length;
484
- completedPlans += Math.min(su.length, pl.length);
485
- if (pl.length > 0 && su.length >= pl.length) completedCount++;
486
- } catch {
487
- /* ok */
488
- }
489
- }
490
- let roadmapPhaseCount = phaseDirs.length;
491
- try {
492
- const rc = fs.readFileSync(path.join(wsDir, "ROADMAP.md"), "utf-8");
493
- const pm = rc.match(/^###?\s+Phase\s+\d/gm);
494
- if (pm) roadmapPhaseCount = pm.length;
495
- } catch {
496
- /* ok */
497
- }
498
- let status = "unknown",
499
- currentPhase = null;
500
- try {
501
- const sc = fs.readFileSync(path.join(wsDir, "STATE.md"), "utf-8");
502
- status = stateExtractField(sc, "Status") || "unknown";
503
- currentPhase = stateExtractField(sc, "Current Phase");
504
- } catch {
505
- /* ok */
506
- }
507
- workstreams.push({
508
- name: entry.name,
509
- active: entry.name === active,
510
- status,
511
- current_phase: currentPhase,
512
- phases: `${completedCount}/${roadmapPhaseCount}`,
513
- plans: `${completedPlans}/${totalPlans}`,
514
- progress_percent:
515
- roadmapPhaseCount > 0
516
- ? Math.round((completedCount / roadmapPhaseCount) * 100)
517
- : 0,
518
- });
519
- }
520
- output(
521
- { mode: "workstream", active, workstreams, count: workstreams.length },
522
- raw,
523
- );
524
- }
package/src/output.ts DELETED
@@ -1,45 +0,0 @@
1
- /**
2
- * output.ts - Format and emit command results.
3
- *
4
- * Every command returns a plain JS value. This module formats it based on
5
- * --output (json | toon) and optionally extracts a sub-value via --pick
6
- * (JSONPath expression).
7
- */
8
-
9
- import { JSONPath } from "jsonpath-plus";
10
-
11
- export type OutputFormat = "json" | "toon";
12
-
13
- // AnyValue replaced with unknown + type guards (TYP-03)
14
- type AnyValue = unknown;
15
-
16
- /**
17
- * Format `value` as a string ready for stdout.
18
- *
19
- * @param value - Any serialisable value returned by a command
20
- * @param format - 'json' (default) or 'toon'
21
- * @param pick - Optional JSONPath expression, e.g. "$.phases[*].name"
22
- */
23
- export function formatOutput(
24
- value: AnyValue,
25
- format: OutputFormat,
26
- pick?: string,
27
- ): string {
28
- let data: AnyValue = value;
29
-
30
- if (pick) {
31
- const result = JSONPath({ path: pick, json: value as object, wrap: false });
32
- data = result;
33
- }
34
-
35
- if (format === "json") {
36
- return JSON.stringify(data, null, 2);
37
- }
38
-
39
- // @toon-format/toon may not ship types; import dynamically to avoid hard failure
40
- // eslint-disable-next-line @typescript-eslint/no-require-imports
41
- const { encode } = require("@toon-format/toon") as {
42
- encode: (v: AnyValue) => string;
43
- };
44
- return encode(data);
45
- }
@@ -1,80 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://pi-gsd.dev/schemas/pi-gsd-settings.schema.json",
4
- "title": "pi-gsd Settings",
5
- "description": "Security and allowlist configuration for the pi-gsd WXP engine. Applies at global (~/.gsd/) or project (.pi/gsd/) scope; project settings override global.",
6
- "type": "object",
7
- "additionalProperties": false,
8
- "definitions": {
9
- "TrustedPathEntry": {
10
- "type": "object",
11
- "required": ["position", "path"],
12
- "additionalProperties": false,
13
- "properties": {
14
- "position": {
15
- "type": "string",
16
- "enum": ["project", "pkg", "absolute"],
17
- "description": "How to resolve the path: 'project' relative to project root, 'pkg' relative to pi-gsd package root, 'absolute' as-is."
18
- },
19
- "path": {
20
- "type": "string",
21
- "description": "The path (resolved according to 'position')."
22
- }
23
- },
24
- "examples": [
25
- { "position": "project", "path": ".pi/gsd" },
26
- { "position": "pkg", "path": ".gsd/harnesses/pi/get-shit-done" },
27
- { "position": "absolute", "path": "/home/user/shared-workflows" }
28
- ]
29
- }
30
- },
31
- "properties": {
32
- "trustedPaths": {
33
- "type": "array",
34
- "items": { "$ref": "#/definitions/TrustedPathEntry" },
35
- "description": "Additional paths trusted for WXP processing. Package harness and project harness are always trusted by default.",
36
- "default": []
37
- },
38
- "untrustedPaths": {
39
- "type": "array",
40
- "items": { "$ref": "#/definitions/TrustedPathEntry" },
41
- "description": "Paths explicitly blocked from WXP processing. Overrides trustedPaths (including defaults). Use to ban a subdirectory of a trusted path.",
42
- "default": [],
43
- "examples": [[{ "position": "project", "path": ".planning" }]]
44
- },
45
- "shellAllowlist": {
46
- "type": "array",
47
- "items": { "type": "string" },
48
- "description": "Additional bare command names allowed in <shell> blocks. Extends the default: [pi-gsd-tools, git, node, cat, ls, echo, find]. Cannot remove defaults — use shellBanlist for that.",
49
- "default": [],
50
- "examples": [["jq"]]
51
- },
52
- "shellBanlist": {
53
- "type": "array",
54
- "items": { "type": "string" },
55
- "description": "Bare command names explicitly blocked in <shell> blocks. Overrides shellAllowlist and defaults.",
56
- "default": [],
57
- "examples": [["bash", "sh", "zsh"]]
58
- },
59
- "shellTimeoutMs": {
60
- "type": "number",
61
- "minimum": 1000,
62
- "maximum": 300000,
63
- "description": "Timeout in milliseconds per <shell> command execution. Default: 30000 (30s).",
64
- "default": 30000
65
- }
66
- },
67
- "examples": [
68
- {
69
- "trustedPaths": [
70
- { "position": "absolute", "path": "/home/user/shared-workflows" }
71
- ],
72
- "untrustedPaths": [
73
- { "position": "project", "path": ".planning" }
74
- ],
75
- "shellAllowlist": ["jq"],
76
- "shellBanlist": [],
77
- "shellTimeoutMs": 30000
78
- }
79
- ]
80
- }