gsd-antigravity-kit 1.27.3 → 1.30.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 (56) hide show
  1. package/.agent/skills/gsd/SKILL.md +2 -2
  2. package/.agent/skills/gsd/assets/templates/config.json +2 -1
  3. package/.agent/skills/gsd/bin/gsd-tools.cjs +20 -1
  4. package/.agent/skills/gsd/bin/help-manifest.json +1 -1
  5. package/.agent/skills/gsd/bin/hooks/gsd-check-update.js +1 -1
  6. package/.agent/skills/gsd/bin/hooks/gsd-context-monitor.js +1 -1
  7. package/.agent/skills/gsd/bin/hooks/gsd-prompt-guard.js +1 -1
  8. package/.agent/skills/gsd/bin/hooks/gsd-statusline.js +1 -1
  9. package/.agent/skills/gsd/bin/hooks/gsd-workflow-guard.js +1 -1
  10. package/.agent/skills/gsd/bin/lib/config.cjs +24 -3
  11. package/.agent/skills/gsd/bin/lib/core.cjs +67 -3
  12. package/.agent/skills/gsd/bin/lib/frontmatter.cjs +56 -27
  13. package/.agent/skills/gsd/bin/lib/init.cjs +109 -3
  14. package/.agent/skills/gsd/bin/lib/security.cjs +26 -0
  15. package/.agent/skills/gsd/bin/lib/state.cjs +67 -5
  16. package/.agent/skills/gsd/bin/lib/uat.cjs +94 -1
  17. package/.agent/skills/gsd/bin/lib/verify.cjs +38 -1
  18. package/.agent/skills/gsd/references/agents/gsd-debugger.md +1 -0
  19. package/.agent/skills/gsd/references/agents/gsd-executor.md +1 -0
  20. package/.agent/skills/gsd/references/agents/gsd-verifier.md +1 -1
  21. package/.agent/skills/gsd/references/commands/debug.md +5 -0
  22. package/.agent/skills/gsd/references/commands/research-phase.md +5 -0
  23. package/.agent/skills/gsd/references/workflows/add-tests.md +2 -2
  24. package/.agent/skills/gsd/references/workflows/add-todo.md +1 -1
  25. package/.agent/skills/gsd/references/workflows/audit-milestone.md +9 -1
  26. package/.agent/skills/gsd/references/workflows/autonomous.md +85 -9
  27. package/.agent/skills/gsd/references/workflows/cleanup.md +2 -2
  28. package/.agent/skills/gsd/references/workflows/complete-milestone.md +4 -3
  29. package/.agent/skills/gsd/references/workflows/diagnose-issues.md +12 -1
  30. package/.agent/skills/gsd/references/workflows/discuss-phase-assumptions.md +14 -6
  31. package/.agent/skills/gsd/references/workflows/discuss-phase.md +13 -11
  32. package/.agent/skills/gsd/references/workflows/execute-phase.md +9 -1
  33. package/.agent/skills/gsd/references/workflows/execute-plan.md +10 -5
  34. package/.agent/skills/gsd/references/workflows/health.md +1 -1
  35. package/.agent/skills/gsd/references/workflows/manager.md +2 -0
  36. package/.agent/skills/gsd/references/workflows/map-codebase.md +16 -9
  37. package/.agent/skills/gsd/references/workflows/new-milestone.md +17 -0
  38. package/.agent/skills/gsd/references/workflows/new-project.md +24 -0
  39. package/.agent/skills/gsd/references/workflows/pause-work.md +2 -2
  40. package/.agent/skills/gsd/references/workflows/plan-milestone-gaps.md +1 -1
  41. package/.agent/skills/gsd/references/workflows/plan-phase.md +13 -2
  42. package/.agent/skills/gsd/references/workflows/plant-seed.md +1 -1
  43. package/.agent/skills/gsd/references/workflows/progress.md +5 -5
  44. package/.agent/skills/gsd/references/workflows/quick.md +25 -0
  45. package/.agent/skills/gsd/references/workflows/research-phase.md +9 -1
  46. package/.agent/skills/gsd/references/workflows/resume-project.md +5 -4
  47. package/.agent/skills/gsd/references/workflows/review.md +1 -1
  48. package/.agent/skills/gsd/references/workflows/transition.md +7 -7
  49. package/.agent/skills/gsd/references/workflows/ui-phase.md +12 -0
  50. package/.agent/skills/gsd/references/workflows/ui-review.md +8 -0
  51. package/.agent/skills/gsd/references/workflows/update.md +1 -1
  52. package/.agent/skills/gsd/references/workflows/validate-phase.md +8 -1
  53. package/.agent/skills/gsd/references/workflows/verify-phase.md +3 -3
  54. package/.agent/skills/gsd/references/workflows/verify-work.md +29 -15
  55. package/README.md +2 -2
  56. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gsd
3
- version: 1.28.0
3
+ version: 1.30.0
4
4
  description: "Antigravity GSD (Get Stuff Done) - A spec-driven hierarchical planning and execution system. Triggers on project planning, phase management, and GSD slash commands."
5
5
  ---
6
6
 
@@ -183,4 +183,4 @@ General documentation on the GSD philosophy, usage patterns, and configuration.
183
183
  5. **CLI Invocation**: `gsd-tools` is **NOT** a global command. Always invoke it with the full node path: `node .agent/skills/gsd/bin/gsd-tools.cjs <command> [args]`. Never run `gsd-tools` bare.
184
184
 
185
185
  ---
186
- *Generated by gsd-converter on 2026-03-22*
186
+ *Generated by gsd-converter on 2026-03-28*
@@ -39,5 +39,6 @@
39
39
  },
40
40
  "hooks": {
41
41
  "context_warnings": true
42
- }
42
+ },
43
+ "agent_skills": {}
43
44
  }
@@ -458,6 +458,11 @@ async function runCommand(command, args, cwd, raw) {
458
458
  break;
459
459
  }
460
460
 
461
+ case 'agent-skills': {
462
+ init.cmdAgentSkills(cwd, args[1], raw);
463
+ break;
464
+ }
465
+
461
466
  case 'history-digest': {
462
467
  commands.cmdHistoryDigest(cwd, raw);
463
468
  break;
@@ -553,8 +558,10 @@ async function runCommand(command, args, cwd, raw) {
553
558
  } else if (subcommand === 'health') {
554
559
  const repairFlag = args.includes('--repair');
555
560
  verify.cmdValidateHealth(cwd, { repair: repairFlag }, raw);
561
+ } else if (subcommand === 'agents') {
562
+ verify.cmdValidateAgents(cwd, raw);
556
563
  } else {
557
- error('Unknown validate subcommand. Available: consistency, health');
564
+ error('Unknown validate subcommand. Available: consistency, health, agents');
558
565
  }
559
566
  break;
560
567
  }
@@ -571,6 +578,18 @@ async function runCommand(command, args, cwd, raw) {
571
578
  break;
572
579
  }
573
580
 
581
+ case 'uat': {
582
+ const subcommand = args[1];
583
+ const uat = require('./lib/uat.cjs');
584
+ if (subcommand === 'render-checkpoint') {
585
+ const options = parseNamedArgs(args, ['file']);
586
+ uat.cmdRenderCheckpoint(cwd, options, raw);
587
+ } else {
588
+ error('Unknown uat subcommand. Available: render-checkpoint');
589
+ }
590
+ break;
591
+ }
592
+
574
593
  case 'stats': {
575
594
  const subcommand = args[1] || 'json';
576
595
  commands.cmdStats(cwd, subcommand, raw);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.28.0",
2
+ "version": "1.30.0",
3
3
  "commands": {
4
4
  "add-backlog": {
5
5
  "description": "Add an idea to the backlog parking lot (999.x numbering)"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // gsd-hook-version: 1.28.0
2
+ // gsd-hook-version: 1.30.0
3
3
  // Check for GSD updates in background, write result to cache
4
4
  // Called by SessionStart hook - runs once per session
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // gsd-hook-version: 1.28.0
2
+ // gsd-hook-version: 1.30.0
3
3
  // Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool)
4
4
  // Reads context metrics from the statusline bridge file and injects
5
5
  // warnings when context usage is high. This makes the AGENT aware of
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // gsd-hook-version: 1.28.0
2
+ // gsd-hook-version: 1.30.0
3
3
  // GSD Prompt Injection Guard — PreToolUse hook
4
4
  // Scans file content being written to .planning/ for prompt injection patterns.
5
5
  // Defense-in-depth: catches injected instructions before they enter agent context.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // gsd-hook-version: 1.28.0
2
+ // gsd-hook-version: 1.30.0
3
3
  // Antigravity Statusline - GSD Edition
4
4
  // Shows: model | current task | directory | context usage
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // gsd-hook-version: 1.28.0
2
+ // gsd-hook-version: 1.30.0
3
3
  // GSD Workflow Guard — PreToolUse hook
4
4
  // Detects when Antigravity attempts file edits outside a GSD workflow context
5
5
  // (no active /gsd: command or Task subagent) and injects an advisory warning.
@@ -27,6 +27,18 @@ const VALID_CONFIG_KEYS = new Set([
27
27
  'hooks.context_warnings',
28
28
  ]);
29
29
 
30
+ /**
31
+ * Check whether a config key path is valid.
32
+ * Supports exact matches from VALID_CONFIG_KEYS plus dynamic patterns
33
+ * like `agent_skills.<agent-type>` where the sub-key is freeform.
34
+ */
35
+ function isValidConfigKey(keyPath) {
36
+ if (VALID_CONFIG_KEYS.has(keyPath)) return true;
37
+ // Allow agent_skills.<agent-type> with any agent type string
38
+ if (/^agent_skills\.[a-zA-Z0-9_-]+$/.test(keyPath)) return true;
39
+ return false;
40
+ }
41
+
30
42
  const CONFIG_KEY_SUGGESTIONS = {
31
43
  'workflow.nyquist_validation_enabled': 'workflow.nyquist_validation',
32
44
  'agents.nyquist_validation_enabled': 'workflow.nyquist_validation',
@@ -120,6 +132,7 @@ function buildNewProjectConfig(userChoices) {
120
132
  hooks: {
121
133
  context_warnings: true,
122
134
  },
135
+ agent_skills: {},
123
136
  };
124
137
 
125
138
  // Three-level deep merge: hardcoded <- userDefaults <- choices
@@ -142,6 +155,11 @@ function buildNewProjectConfig(userChoices) {
142
155
  ...(userDefaults.hooks || {}),
143
156
  ...(choices.hooks || {}),
144
157
  },
158
+ agent_skills: {
159
+ ...hardcoded.agent_skills,
160
+ ...(userDefaults.agent_skills || {}),
161
+ ...(choices.agent_skills || {}),
162
+ },
145
163
  };
146
164
  }
147
165
 
@@ -298,15 +316,18 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
298
316
 
299
317
  validateKnownConfigKeyPath(keyPath);
300
318
 
301
- if (!VALID_CONFIG_KEYS.has(keyPath)) {
302
- error(`Unknown config key: "${keyPath}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}`);
319
+ if (!isValidConfigKey(keyPath)) {
320
+ error(`Unknown config key: "${keyPath}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>`);
303
321
  }
304
322
 
305
- // Parse value (handle booleans and numbers)
323
+ // Parse value (handle booleans, numbers, and JSON arrays/objects)
306
324
  let parsedValue = value;
307
325
  if (value === 'true') parsedValue = true;
308
326
  else if (value === 'false') parsedValue = false;
309
327
  else if (!isNaN(value) && value !== '') parsedValue = Number(value);
328
+ else if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) {
329
+ try { parsedValue = JSON.parse(value); } catch { /* keep as string */ }
330
+ }
310
331
 
311
332
  const setConfigValueResult = setConfigValue(cwd, keyPath, parsedValue);
312
333
  output(setConfigValueResult, raw, `${keyPath}=${parsedValue}`);
@@ -58,13 +58,22 @@ function findProjectRoot(startDir) {
58
58
  const root = path.parse(resolved).root;
59
59
  const homedir = require('os').homedir();
60
60
 
61
- // Check if startDir or any of its ancestors (up to but not including a
61
+ // If startDir already contains .planning/, it IS the project root.
62
+ // Do not walk up to a parent workspace that also has .planning/ (#1362).
63
+ const ownPlanning = path.join(resolved, '.planning');
64
+ if (fs.existsSync(ownPlanning) && fs.statSync(ownPlanning).isDirectory()) {
65
+ return startDir;
66
+ }
67
+
68
+ // Check if startDir or any of its ancestors (up to AND including the
62
69
  // candidate project root) contains a .git directory. This handles both
63
- // `backend/` (direct sub-repo) and `backend/src/modules/` (nested inside).
70
+ // `backend/` (direct sub-repo) and `backend/src/modules/` (nested inside),
71
+ // as well as the common case where .git lives at the same level as .planning/.
64
72
  function isInsideGitRepo(candidateParent) {
65
73
  let d = resolved;
66
- while (d !== candidateParent && d !== root) {
74
+ while (d !== root) {
67
75
  if (fs.existsSync(path.join(d, '.git'))) return true;
76
+ if (d === candidateParent) break;
68
77
  d = path.dirname(d);
69
78
  }
70
79
  return false;
@@ -355,6 +364,7 @@ function loadConfig(cwd) {
355
364
  context_window: get('context_window') ?? defaults.context_window,
356
365
  phase_naming: get('phase_naming') ?? defaults.phase_naming,
357
366
  model_overrides: parsed.model_overrides || null,
367
+ agent_skills: parsed.agent_skills || {},
358
368
  };
359
369
  } catch {
360
370
  return defaults;
@@ -977,6 +987,58 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
977
987
  }
978
988
  }
979
989
 
990
+ // ─── Agent installation validation (#1371) ───────────────────────────────────
991
+
992
+ /**
993
+ * Resolve the agents directory from the GSD install location.
994
+ * gsd-tools.cjs lives at <configDir>/get-shit-done/bin/gsd-tools.cjs,
995
+ * so agents/ is at <configDir>/agents/.
996
+ *
997
+ * @returns {string} Absolute path to the agents directory
998
+ */
999
+ function getAgentsDir() {
1000
+ // __dirname is get-shit-done/bin/lib/ → go up 3 levels to configDir
1001
+ return path.join(__dirname, '..', '..', '..', 'agents');
1002
+ }
1003
+
1004
+ /**
1005
+ * Check which GSD agents are installed on disk.
1006
+ * Returns an object with installation status and details.
1007
+ *
1008
+ * @returns {{ agents_installed: boolean, missing_agents: string[], installed_agents: string[], agents_dir: string }}
1009
+ */
1010
+ function checkAgentsInstalled() {
1011
+ const agentsDir = getAgentsDir();
1012
+ const expectedAgents = Object.keys(MODEL_PROFILES);
1013
+ const installed = [];
1014
+ const missing = [];
1015
+
1016
+ if (!fs.existsSync(agentsDir)) {
1017
+ return {
1018
+ agents_installed: false,
1019
+ missing_agents: expectedAgents,
1020
+ installed_agents: [],
1021
+ agents_dir: agentsDir,
1022
+ };
1023
+ }
1024
+
1025
+ for (const agent of expectedAgents) {
1026
+ const agentFile = path.join(agentsDir, `${agent}.md`);
1027
+ if (fs.existsSync(agentFile)) {
1028
+ installed.push(agent);
1029
+ } else {
1030
+ missing.push(agent);
1031
+ }
1032
+ }
1033
+
1034
+ return {
1035
+ agents_installed: installed.length > 0 && missing.length === 0,
1036
+ missing_agents: missing,
1037
+ installed_agents: installed,
1038
+ agents_dir: agentsDir,
1039
+ };
1040
+ }
1041
+
980
1042
  // ─── Model alias resolution ───────────────────────────────────────────────────
981
1043
 
982
1044
  /**
@@ -1222,4 +1284,6 @@ module.exports = {
1222
1284
  filterSummaryFiles,
1223
1285
  getPhaseFileStats,
1224
1286
  readSubdirectories,
1287
+ getAgentsDir,
1288
+ checkAgentsInstalled,
1225
1289
  };
@@ -167,57 +167,86 @@ function parseMustHavesBlock(content, blockName) {
167
167
  if (!fmMatch) return [];
168
168
 
169
169
  const yaml = fmMatch[1];
170
- // Find the block (e.g., "truths:", "artifacts:", "key_links:")
171
- const blockPattern = new RegExp(`^\\s{4}${blockName}:\\s*$`, 'm');
172
- const blockStart = yaml.search(blockPattern);
170
+
171
+ // Find must_haves: first to detect its indentation level
172
+ const mustHavesMatch = yaml.match(/^(\s*)must_haves:\s*$/m);
173
+ if (!mustHavesMatch) return [];
174
+ const mustHavesIndent = mustHavesMatch[1].length;
175
+
176
+ // Find the block (e.g., "truths:", "artifacts:", "key_links:") under must_haves
177
+ // It must be indented more than must_haves but we detect the actual indent dynamically
178
+ const blockPattern = new RegExp(`^(\\s+)${blockName}:\\s*$`, 'm');
179
+ const blockMatch = yaml.match(blockPattern);
180
+ if (!blockMatch) return [];
181
+
182
+ const blockIndent = blockMatch[1].length;
183
+ // The block must be nested under must_haves (more indented)
184
+ if (blockIndent <= mustHavesIndent) return [];
185
+
186
+ // Find where the block starts in the yaml string
187
+ const blockStart = yaml.indexOf(blockMatch[0]);
173
188
  if (blockStart === -1) return [];
174
189
 
175
190
  const afterBlock = yaml.slice(blockStart);
176
191
  const blockLines = afterBlock.split(/\r?\n/).slice(1); // skip the header line
177
192
 
193
+ // List items are indented one level deeper than blockIndent
194
+ // Continuation KVs are indented one level deeper than list items
178
195
  const items = [];
179
196
  let current = null;
197
+ let listItemIndent = -1; // detected from first "- " line
180
198
 
181
199
  for (const line of blockLines) {
182
- // Stop at same or lower indent level (non-continuation)
200
+ // Skip empty lines
183
201
  if (line.trim() === '') continue;
184
202
  const indent = line.match(/^(\s*)/)[1].length;
185
- if (indent <= 4 && line.trim() !== '') break; // back to must_haves level or higher
203
+ // Stop at same or lower indent level than the block header
204
+ if (indent <= blockIndent && line.trim() !== '') break;
205
+
206
+ const trimmed = line.trim();
186
207
 
187
- if (line.match(/^\s{6}-\s+/)) {
188
- // New list item at 6-space indent
208
+ if (trimmed.startsWith('- ')) {
209
+ // Detect list item indent from the first occurrence
210
+ if (listItemIndent === -1) listItemIndent = indent;
211
+
212
+ // Only treat as a top-level list item if at the expected indent
213
+ if (indent === listItemIndent) {
189
214
  if (current) items.push(current);
190
215
  current = {};
191
- // Check if it's a simple string item
192
- const simpleMatch = line.match(/^\s{6}-\s+"?([^"]+)"?\s*$/);
193
- if (simpleMatch && !line.includes(':')) {
194
- current = simpleMatch[1];
216
+ const afterDash = trimmed.slice(2);
217
+ // Check if it's a simple string item (no colon means not a key-value)
218
+ if (!afterDash.includes(':')) {
219
+ current = afterDash.replace(/^["']|["']$/g, '');
195
220
  } else {
196
- // Key-value on same line as dash: "- path: value"
197
- const kvMatch = line.match(/^\s{6}-\s+(\w+):\s*"?([^"]*)"?\s*$/);
198
- if (kvMatch) {
221
+ // Key-value on same line as dash: "- path: value"
222
+ const kvMatch = afterDash.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
223
+ if (kvMatch) {
199
224
  current = {};
200
225
  current[kvMatch[1]] = kvMatch[2];
226
+ }
201
227
  }
228
+ continue;
202
229
  }
203
- } else if (current && typeof current === 'object') {
204
- // Continuation key-value at 8+ space indent
205
- const kvMatch = line.match(/^\s{8,}(\w+):\s*"?([^"]*)"?\s*$/);
206
- if (kvMatch) {
207
- const val = kvMatch[2];
208
- // Try to parse as number
209
- current[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
210
- }
211
- // Array items under a key
212
- const arrMatch = line.match(/^\s{10,}-\s+"?([^"]+)"?\s*$/);
213
- if (arrMatch) {
214
- // Find the last key added and convert to array
230
+ }
231
+
232
+ if (current && typeof current === 'object' && indent > listItemIndent) {
233
+ // Continuation key-value or nested array item
234
+ if (trimmed.startsWith('- ')) {
235
+ // Array item under a key
236
+ const arrVal = trimmed.slice(2).replace(/^["']|["']$/g, '');
215
237
  const keys = Object.keys(current);
216
238
  const lastKey = keys[keys.length - 1];
217
239
  if (lastKey && !Array.isArray(current[lastKey])) {
218
240
  current[lastKey] = current[lastKey] ? [current[lastKey]] : [];
219
241
  }
220
- if (lastKey) current[lastKey].push(arrMatch[1]);
242
+ if (lastKey) current[lastKey].push(arrVal);
243
+ } else {
244
+ const kvMatch = trimmed.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
245
+ if (kvMatch) {
246
+ const val = kvMatch[2];
247
+ // Try to parse as number
248
+ current[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
249
+ }
221
250
  }
222
251
  }
223
252
  }
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { execSync } = require('child_process');
8
- const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, planningPaths, planningDir, planningRoot, toPosixPath, output, error , buildPhaseBase, applyIncludes } = require('./core.cjs');
8
+ const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, planningPaths, planningDir, planningRoot, toPosixPath, output, error, checkAgentsInstalled , buildPhaseBase, applyIncludes } = require('./core.cjs');
9
9
 
10
10
  function getLatestCompletedMilestone(cwd) {
11
11
  const milestonesPath = path.join(planningRoot(cwd), 'MILESTONES.md');
@@ -31,6 +31,12 @@ function getLatestCompletedMilestone(cwd) {
31
31
  */
32
32
  function withProjectRoot(cwd, result) {
33
33
  result.project_root = cwd;
34
+ // Inject agent installation status into all init outputs (#1371).
35
+ // Workflows that spawn named subagents use this to detect when agents
36
+ // are missing and would silently fall back to general-purpose.
37
+ const agentStatus = checkAgentsInstalled();
38
+ result.agents_installed = agentStatus.agents_installed;
39
+ result.missing_agents = agentStatus.missing_agents;
34
40
  return result;
35
41
  }
36
42
 
@@ -242,7 +248,23 @@ function cmdInitNewProject(cwd, raw) {
242
248
  let hasCode = false;
243
249
  let hasPackageFile = false;
244
250
  try {
245
- const codeExtensions = new Set(['.ts', '.js', '.py', '.go', '.rs', '.swift', '.java']);
251
+ const codeExtensions = new Set([
252
+ '.ts', '.js', '.py', '.go', '.rs', '.swift', '.java',
253
+ '.kt', '.kts', // Kotlin (Android, server-side)
254
+ '.c', '.cpp', '.h', // C/C++
255
+ '.cs', // C#
256
+ '.rb', // Ruby
257
+ '.php', // PHP
258
+ '.dart', // Dart (Flutter)
259
+ '.m', '.mm', // Objective-C / Objective-C++
260
+ '.scala', // Scala
261
+ '.groovy', // Groovy (Gradle build scripts)
262
+ '.lua', // Lua
263
+ '.r', '.R', // R
264
+ '.zig', // Zig
265
+ '.ex', '.exs', // Elixir
266
+ '.clj', // Clojure
267
+ ]);
246
268
  const skipDirs = new Set(['node_modules', '.git', '.planning', '.antigravity', '__pycache__', 'target', 'dist', 'build']);
247
269
  function findCodeFiles(dir, depth) {
248
270
  if (depth > 3) return false;
@@ -263,7 +285,18 @@ function cmdInitNewProject(cwd, raw) {
263
285
  pathExistsInternal(cwd, 'requirements.txt') ||
264
286
  pathExistsInternal(cwd, 'Cargo.toml') ||
265
287
  pathExistsInternal(cwd, 'go.mod') ||
266
- pathExistsInternal(cwd, 'Package.swift');
288
+ pathExistsInternal(cwd, 'Package.swift') ||
289
+ pathExistsInternal(cwd, 'build.gradle') ||
290
+ pathExistsInternal(cwd, 'build.gradle.kts') ||
291
+ pathExistsInternal(cwd, 'pom.xml') ||
292
+ pathExistsInternal(cwd, 'Gemfile') ||
293
+ pathExistsInternal(cwd, 'composer.json') ||
294
+ pathExistsInternal(cwd, 'pubspec.yaml') ||
295
+ pathExistsInternal(cwd, 'CMakeLists.txt') ||
296
+ pathExistsInternal(cwd, 'Makefile') ||
297
+ pathExistsInternal(cwd, 'build.zig') ||
298
+ pathExistsInternal(cwd, 'mix.exs') ||
299
+ pathExistsInternal(cwd, 'project.clj');
267
300
 
268
301
  const result = {
269
302
  // Models
@@ -1301,6 +1334,77 @@ function cmdInitRemoveWorkspace(cwd, name, raw) {
1301
1334
  output(result, raw);
1302
1335
  }
1303
1336
 
1337
+ /**
1338
+ * Build a formatted agent skills block for injection into Task() prompts.
1339
+ *
1340
+ * Reads `config.agent_skills[agentType]` and validates each skill path exists
1341
+ * within the project root. Returns a formatted `<agent_skills>` block or empty
1342
+ * string if no skills are configured.
1343
+ *
1344
+ * @param {object} config - Loaded project config
1345
+ * @param {string} agentType - The agent type (e.g., 'gsd-executor', 'gsd-planner')
1346
+ * @param {string} projectRoot - Absolute path to project root (for path validation)
1347
+ * @returns {string} Formatted skills block or empty string
1348
+ */
1349
+ function buildAgentSkillsBlock(config, agentType, projectRoot) {
1350
+ const { validatePath } = require('./security.cjs');
1351
+
1352
+ if (!config || !config.agent_skills || !agentType) return '';
1353
+
1354
+ let skillPaths = config.agent_skills[agentType];
1355
+ if (!skillPaths) return '';
1356
+
1357
+ // Normalize single string to array
1358
+ if (typeof skillPaths === 'string') skillPaths = [skillPaths];
1359
+ if (!Array.isArray(skillPaths) || skillPaths.length === 0) return '';
1360
+
1361
+ const validPaths = [];
1362
+ for (const skillPath of skillPaths) {
1363
+ if (typeof skillPath !== 'string') continue;
1364
+
1365
+ // Validate path safety — must resolve within project root
1366
+ const pathCheck = validatePath(skillPath, projectRoot);
1367
+ if (!pathCheck.safe) {
1368
+ process.stderr.write(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": ${pathCheck.error}\n`);
1369
+ continue;
1370
+ }
1371
+
1372
+ // Check that the skill directory and SKILL.md exist
1373
+ const skillMdPath = path.join(projectRoot, skillPath, 'SKILL.md');
1374
+ if (!fs.existsSync(skillMdPath)) {
1375
+ process.stderr.write(`[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md" — skipping\n`);
1376
+ continue;
1377
+ }
1378
+
1379
+ validPaths.push(skillPath);
1380
+ }
1381
+
1382
+ if (validPaths.length === 0) return '';
1383
+
1384
+ const lines = validPaths.map(p => `- @${p}/SKILL.md`).join('\n');
1385
+ return `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`;
1386
+ }
1387
+
1388
+ /**
1389
+ * Command: output the agent skills block for a given agent type.
1390
+ * Used by workflows: SKILLS=$(node "$TOOLS" agent-skills gsd-executor 2>/dev/null)
1391
+ */
1392
+ function cmdAgentSkills(cwd, agentType, raw) {
1393
+ if (!agentType) {
1394
+ // No agent type — output empty string silently
1395
+ output('', raw, '');
1396
+ return;
1397
+ }
1398
+
1399
+ const config = loadConfig(cwd);
1400
+ const block = buildAgentSkillsBlock(config, agentType, cwd);
1401
+ // Output raw text (not JSON) so workflows can embed it directly
1402
+ if (block) {
1403
+ process.stdout.write(block);
1404
+ }
1405
+ process.exit(0);
1406
+ }
1407
+
1304
1408
  module.exports = {
1305
1409
  cmdInitExecutePhase,
1306
1410
  cmdInitPlanPhase,
@@ -1319,4 +1423,6 @@ module.exports = {
1319
1423
  cmdInitListWorkspaces,
1320
1424
  cmdInitRemoveWorkspace,
1321
1425
  detectChildRepos,
1426
+ buildAgentSkillsBlock,
1427
+ cmdAgentSkills,
1322
1428
  };
@@ -223,6 +223,31 @@ function sanitizeForPrompt(text) {
223
223
  return sanitized;
224
224
  }
225
225
 
226
+ /**
227
+ * Sanitize text that will be displayed back to the user.
228
+ * Removes protocol-like leak markers that should never surface in checkpoints.
229
+ *
230
+ * @param {string} text - Text to sanitize
231
+ * @returns {string} Sanitized text
232
+ */
233
+ function sanitizeForDisplay(text) {
234
+ if (!text || typeof text !== 'string') return text;
235
+
236
+ let sanitized = sanitizeForPrompt(text);
237
+
238
+ const protocolLeakPatterns = [
239
+ /^\s*(?:assistant|user|system)\s+to=[^:\s]+:[^\n]+$/i,
240
+ /^\s*<\|(?:assistant|user|system)[^|]*\|>\s*$/i,
241
+ ];
242
+
243
+ sanitized = sanitized
244
+ .split('\n')
245
+ .filter(line => !protocolLeakPatterns.some(pattern => pattern.test(line)))
246
+ .join('\n');
247
+
248
+ return sanitized;
249
+ }
250
+
226
251
  // ─── Shell Safety ───────────────────────────────────────────────────────────
227
252
 
228
253
  /**
@@ -343,6 +368,7 @@ module.exports = {
343
368
  INJECTION_PATTERNS,
344
369
  scanForInjection,
345
370
  sanitizeForPrompt,
371
+ sanitizeForDisplay,
346
372
 
347
373
  // Shell safety
348
374
  validateShellArg,