gsd-opencode 1.30.0 → 1.33.1
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/agents/gsd-debugger.md +0 -1
- package/agents/gsd-doc-verifier.md +207 -0
- package/agents/gsd-doc-writer.md +608 -0
- package/agents/gsd-executor.md +22 -1
- package/agents/gsd-phase-researcher.md +41 -0
- package/agents/gsd-plan-checker.md +82 -0
- package/agents/gsd-planner.md +123 -194
- package/agents/gsd-security-auditor.md +129 -0
- package/agents/gsd-ui-auditor.md +40 -0
- package/agents/gsd-user-profiler.md +2 -2
- package/agents/gsd-verifier.md +84 -18
- package/commands/gsd/gsd-add-backlog.md +1 -1
- package/commands/gsd/gsd-analyze-dependencies.md +34 -0
- package/commands/gsd/gsd-autonomous.md +6 -2
- package/commands/gsd/gsd-cleanup.md +5 -0
- package/commands/gsd/gsd-debug.md +24 -21
- package/commands/gsd/gsd-discuss-phase.md +7 -2
- package/commands/gsd/gsd-docs-update.md +48 -0
- package/commands/gsd/gsd-execute-phase.md +4 -0
- package/commands/gsd/gsd-help.md +2 -0
- package/commands/gsd/gsd-join-discord.md +2 -1
- package/commands/gsd/gsd-manager.md +1 -0
- package/commands/gsd/gsd-new-project.md +4 -0
- package/commands/gsd/gsd-plan-phase.md +5 -0
- package/commands/gsd/gsd-quick.md +5 -3
- package/commands/gsd/gsd-reapply-patches.md +171 -39
- package/commands/gsd/gsd-research-phase.md +2 -12
- package/commands/gsd/gsd-review-backlog.md +1 -0
- package/commands/gsd/gsd-review.md +3 -2
- package/commands/gsd/gsd-secure-phase.md +35 -0
- package/commands/gsd/gsd-set-profile.md +0 -1
- package/commands/gsd/gsd-thread.md +1 -1
- package/commands/gsd/gsd-workstreams.md +7 -2
- package/get-shit-done/bin/gsd-tools.cjs +42 -8
- package/get-shit-done/bin/lib/commands.cjs +68 -14
- package/get-shit-done/bin/lib/config.cjs +18 -10
- package/get-shit-done/bin/lib/core.cjs +383 -80
- package/get-shit-done/bin/lib/docs.cjs +267 -0
- package/get-shit-done/bin/lib/frontmatter.cjs +47 -2
- package/get-shit-done/bin/lib/init.cjs +85 -5
- package/get-shit-done/bin/lib/milestone.cjs +21 -0
- package/get-shit-done/bin/lib/model-profiles.cjs +2 -0
- package/get-shit-done/bin/lib/phase.cjs +232 -189
- package/get-shit-done/bin/lib/profile-output.cjs +97 -1
- package/get-shit-done/bin/lib/roadmap.cjs +137 -113
- package/get-shit-done/bin/lib/schema-detect.cjs +238 -0
- package/get-shit-done/bin/lib/security.cjs +5 -3
- package/get-shit-done/bin/lib/state.cjs +366 -44
- package/get-shit-done/bin/lib/verify.cjs +158 -14
- package/get-shit-done/bin/lib/workstream.cjs +6 -2
- package/get-shit-done/references/agent-contracts.md +79 -0
- package/get-shit-done/references/artifact-types.md +113 -0
- package/get-shit-done/references/context-budget.md +49 -0
- package/get-shit-done/references/continuation-format.md +15 -15
- package/get-shit-done/references/domain-probes.md +125 -0
- package/get-shit-done/references/gate-prompts.md +100 -0
- package/get-shit-done/references/model-profiles.md +2 -2
- package/get-shit-done/references/planner-gap-closure.md +62 -0
- package/get-shit-done/references/planner-reviews.md +39 -0
- package/get-shit-done/references/planner-revision.md +87 -0
- package/get-shit-done/references/planning-config.md +15 -0
- package/get-shit-done/references/revision-loop.md +97 -0
- package/get-shit-done/references/ui-brand.md +2 -2
- package/get-shit-done/references/universal-anti-patterns.md +58 -0
- package/get-shit-done/references/workstream-flag.md +56 -3
- package/get-shit-done/templates/SECURITY.md +61 -0
- package/get-shit-done/templates/VALIDATION.md +3 -3
- package/get-shit-done/templates/claude-md.md +27 -4
- package/get-shit-done/templates/config.json +4 -0
- package/get-shit-done/templates/debug-subagent-prompt.md +2 -6
- package/get-shit-done/templates/planner-subagent-prompt.md +2 -10
- package/get-shit-done/workflows/add-phase.md +2 -2
- package/get-shit-done/workflows/add-todo.md +1 -1
- package/get-shit-done/workflows/analyze-dependencies.md +96 -0
- package/get-shit-done/workflows/audit-milestone.md +8 -12
- package/get-shit-done/workflows/autonomous.md +158 -13
- package/get-shit-done/workflows/check-todos.md +2 -2
- package/get-shit-done/workflows/complete-milestone.md +13 -4
- package/get-shit-done/workflows/diagnose-issues.md +8 -6
- package/get-shit-done/workflows/discovery-phase.md +1 -1
- package/get-shit-done/workflows/discuss-phase-assumptions.md +24 -6
- package/get-shit-done/workflows/discuss-phase-power.md +291 -0
- package/get-shit-done/workflows/discuss-phase.md +153 -20
- package/get-shit-done/workflows/docs-update.md +1093 -0
- package/get-shit-done/workflows/execute-phase.md +362 -66
- package/get-shit-done/workflows/execute-plan.md +1 -1
- package/get-shit-done/workflows/help.md +9 -6
- package/get-shit-done/workflows/insert-phase.md +2 -2
- package/get-shit-done/workflows/manager.md +27 -26
- package/get-shit-done/workflows/map-codebase.md +10 -32
- package/get-shit-done/workflows/new-milestone.md +14 -8
- package/get-shit-done/workflows/new-project.md +48 -25
- package/get-shit-done/workflows/next.md +1 -1
- package/get-shit-done/workflows/note.md +1 -1
- package/get-shit-done/workflows/pause-work.md +73 -10
- package/get-shit-done/workflows/plan-milestone-gaps.md +2 -2
- package/get-shit-done/workflows/plan-phase.md +184 -32
- package/get-shit-done/workflows/progress.md +20 -20
- package/get-shit-done/workflows/quick.md +102 -84
- package/get-shit-done/workflows/research-phase.md +2 -6
- package/get-shit-done/workflows/resume-project.md +4 -4
- package/get-shit-done/workflows/review.md +56 -3
- package/get-shit-done/workflows/secure-phase.md +154 -0
- package/get-shit-done/workflows/settings.md +13 -2
- package/get-shit-done/workflows/ship.md +13 -4
- package/get-shit-done/workflows/transition.md +6 -6
- package/get-shit-done/workflows/ui-phase.md +4 -14
- package/get-shit-done/workflows/ui-review.md +25 -7
- package/get-shit-done/workflows/update.md +165 -16
- package/get-shit-done/workflows/validate-phase.md +1 -11
- package/get-shit-done/workflows/verify-phase.md +127 -6
- package/get-shit-done/workflows/verify-work.md +69 -21
- package/package.json +1 -1
|
@@ -177,8 +177,12 @@ const CLAUDE_MD_FALLBACKS = {
|
|
|
177
177
|
stack: 'Technology stack not yet documented. Will populate after codebase mapping or first phase.',
|
|
178
178
|
conventions: 'Conventions not yet established. Will populate as patterns emerge during development.',
|
|
179
179
|
architecture: 'Architecture not yet mapped. Follow existing patterns found in the codebase.',
|
|
180
|
+
skills: 'No project skills found. Add skills to any of: `.OpenCode/skills/`, `.agents/skills/`, `.cursor/skills/`, or `.github/skills/` with a `SKILL.md` index file.',
|
|
180
181
|
};
|
|
181
182
|
|
|
183
|
+
// Directories where project skills may live (checked in order)
|
|
184
|
+
const SKILL_SEARCH_DIRS = ['.OpenCode/skills', '.agents/skills', '.cursor/skills', '.github/skills'];
|
|
185
|
+
|
|
182
186
|
const CLAUDE_MD_WORKFLOW_ENFORCEMENT = [
|
|
183
187
|
'Before using edit, write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.',
|
|
184
188
|
'',
|
|
@@ -375,6 +379,96 @@ function generateWorkflowSection() {
|
|
|
375
379
|
};
|
|
376
380
|
}
|
|
377
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Discover project skills from standard directories and extract frontmatter
|
|
384
|
+
* (name + description) for each. Returns a table summary for AGENTS.md so
|
|
385
|
+
* agents know which skills are available at session startup (Layer 1 discovery).
|
|
386
|
+
*/
|
|
387
|
+
function generateSkillsSection(cwd) {
|
|
388
|
+
const discovered = [];
|
|
389
|
+
|
|
390
|
+
for (const dir of SKILL_SEARCH_DIRS) {
|
|
391
|
+
const absDir = path.join(cwd, dir);
|
|
392
|
+
if (!fs.existsSync(absDir)) continue;
|
|
393
|
+
|
|
394
|
+
let entries;
|
|
395
|
+
try {
|
|
396
|
+
entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
397
|
+
} catch {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
for (const entry of entries) {
|
|
402
|
+
if (!entry.isDirectory()) continue;
|
|
403
|
+
// Skip GSD's own installed skills — only surface project-specific skills
|
|
404
|
+
if (entry.name.startsWith('gsd-')) continue;
|
|
405
|
+
|
|
406
|
+
const skillMdPath = path.join(absDir, entry.name, 'SKILL.md');
|
|
407
|
+
if (!fs.existsSync(skillMdPath)) continue;
|
|
408
|
+
|
|
409
|
+
const content = safeReadFile(skillMdPath);
|
|
410
|
+
if (!content) continue;
|
|
411
|
+
|
|
412
|
+
const frontmatter = extractSkillFrontmatter(content);
|
|
413
|
+
const name = frontmatter.name || entry.name;
|
|
414
|
+
const description = frontmatter.description || '';
|
|
415
|
+
|
|
416
|
+
// Avoid duplicates when same skill dir is symlinked from multiple locations
|
|
417
|
+
if (discovered.some(s => s.name === name)) continue;
|
|
418
|
+
|
|
419
|
+
discovered.push({ name, description, path: `${dir}/${entry.name}` });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (discovered.length === 0) {
|
|
424
|
+
return { content: CLAUDE_MD_FALLBACKS.skills, source: 'skills/', hasFallback: true };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const lines = ['| skill | Description | Path |', '|-------|-------------|------|'];
|
|
428
|
+
for (const skill of discovered) {
|
|
429
|
+
// Sanitize table cell content (escape pipes)
|
|
430
|
+
const desc = skill.description.replace(/\|/g, '\\|').replace(/\n/g, ' ').trim();
|
|
431
|
+
const safeName = skill.name.replace(/\|/g, '\\|');
|
|
432
|
+
lines.push(`| ${safeName} | ${desc} | \`${skill.path}/SKILL.md\` |`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return { content: lines.join('\n'), source: 'skills/', hasFallback: false };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Extract name and description from YAML-like frontmatter in a SKILL.md file.
|
|
440
|
+
* Handles multi-line description values (continuation lines indented with spaces).
|
|
441
|
+
*/
|
|
442
|
+
function extractSkillFrontmatter(content) {
|
|
443
|
+
const result = { name: '', description: '' };
|
|
444
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
445
|
+
if (!fmMatch) return result;
|
|
446
|
+
|
|
447
|
+
const fmBlock = fmMatch[1];
|
|
448
|
+
const lines = fmBlock.split('\n');
|
|
449
|
+
|
|
450
|
+
let currentKey = '';
|
|
451
|
+
for (const line of lines) {
|
|
452
|
+
// Top-level key: value
|
|
453
|
+
const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)/);
|
|
454
|
+
if (kvMatch) {
|
|
455
|
+
currentKey = kvMatch[1];
|
|
456
|
+
const value = kvMatch[2].trim();
|
|
457
|
+
if (currentKey === 'name') result.name = value;
|
|
458
|
+
if (currentKey === 'description') result.description = value;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
// Continuation line (indented) for multi-line values
|
|
462
|
+
if (currentKey === 'description' && /^\s+/.test(line)) {
|
|
463
|
+
result.description += ' ' + line.trim();
|
|
464
|
+
} else {
|
|
465
|
+
currentKey = '';
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
|
|
378
472
|
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
379
473
|
|
|
380
474
|
function cmdWriteProfile(cwd, options, raw) {
|
|
@@ -815,12 +909,13 @@ function cmdGenerateClaudeProfile(cwd, options, raw) {
|
|
|
815
909
|
}
|
|
816
910
|
|
|
817
911
|
function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
818
|
-
const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture', 'workflow'];
|
|
912
|
+
const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture', 'skills', 'workflow'];
|
|
819
913
|
const generators = {
|
|
820
914
|
project: generateProjectSection,
|
|
821
915
|
stack: generateStackSection,
|
|
822
916
|
conventions: generateConventionsSection,
|
|
823
917
|
architecture: generateArchitectureSection,
|
|
918
|
+
skills: generateSkillsSection,
|
|
824
919
|
workflow: generateWorkflowSection,
|
|
825
920
|
};
|
|
826
921
|
const sectionHeadings = {
|
|
@@ -828,6 +923,7 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
|
828
923
|
stack: '## Technology Stack',
|
|
829
924
|
conventions: '## Conventions',
|
|
830
925
|
architecture: '## Architecture',
|
|
926
|
+
skills: '## Project Skills',
|
|
831
927
|
workflow: '## GSD Workflow Enforcement',
|
|
832
928
|
};
|
|
833
929
|
|
|
@@ -4,7 +4,73 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { escapeRegex, normalizePhaseName, planningPaths, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, normalizePhaseName, planningPaths, withPlanningLock, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, phaseTokenMatches } = require('./core.cjs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Search for a phase header (and its section) within the given content string.
|
|
11
|
+
* Returns a result object if found (either a full match or a malformed_roadmap
|
|
12
|
+
* checklist-only match), or null if the phase is not present at all.
|
|
13
|
+
*/
|
|
14
|
+
function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
15
|
+
// Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
|
|
16
|
+
const phasePattern = new RegExp(
|
|
17
|
+
`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
|
|
18
|
+
'i'
|
|
19
|
+
);
|
|
20
|
+
const headerMatch = content.match(phasePattern);
|
|
21
|
+
|
|
22
|
+
if (!headerMatch) {
|
|
23
|
+
// Fallback: check if phase exists in summary list but missing detail section
|
|
24
|
+
const checklistPattern = new RegExp(
|
|
25
|
+
`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${escapedPhase}:\\s*([^*]+)\\*\\*`,
|
|
26
|
+
'i'
|
|
27
|
+
);
|
|
28
|
+
const checklistMatch = content.match(checklistPattern);
|
|
29
|
+
|
|
30
|
+
if (checklistMatch) {
|
|
31
|
+
return {
|
|
32
|
+
found: false,
|
|
33
|
+
phase_number: phaseNum,
|
|
34
|
+
phase_name: checklistMatch[1].trim(),
|
|
35
|
+
error: 'malformed_roadmap',
|
|
36
|
+
message: `Phase ${phaseNum} exists in summary list but missing "### Phase ${phaseNum}:" detail section. ROADMAP.md needs both formats.`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const phaseName = headerMatch[1].trim();
|
|
44
|
+
const headerIndex = headerMatch.index;
|
|
45
|
+
|
|
46
|
+
// Find the end of this section (next ## or ### phase header, or end of file)
|
|
47
|
+
const restOfContent = content.slice(headerIndex);
|
|
48
|
+
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
49
|
+
const sectionEnd = nextHeaderMatch
|
|
50
|
+
? headerIndex + nextHeaderMatch.index
|
|
51
|
+
: content.length;
|
|
52
|
+
|
|
53
|
+
const section = content.slice(headerIndex, sectionEnd).trim();
|
|
54
|
+
|
|
55
|
+
// Extract goal if present (supports both **Goal:** and **Goal**: formats)
|
|
56
|
+
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
57
|
+
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
58
|
+
|
|
59
|
+
// Extract success criteria as structured array
|
|
60
|
+
const criteriaMatch = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i);
|
|
61
|
+
const success_criteria = criteriaMatch
|
|
62
|
+
? criteriaMatch[1].trim().split('\n').map(line => line.replace(/^\s*\d+\.\s*/, '').trim()).filter(Boolean)
|
|
63
|
+
: [];
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
found: true,
|
|
67
|
+
phase_number: phaseNum,
|
|
68
|
+
phase_name: phaseName,
|
|
69
|
+
goal,
|
|
70
|
+
success_criteria,
|
|
71
|
+
section,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
8
74
|
|
|
9
75
|
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
10
76
|
const roadmapPath = planningPaths(cwd).roadmap;
|
|
@@ -15,76 +81,32 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
|
15
81
|
}
|
|
16
82
|
|
|
17
83
|
try {
|
|
18
|
-
const
|
|
84
|
+
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
85
|
+
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
19
86
|
|
|
20
87
|
// Escape special regex chars in phase number, handle decimal
|
|
21
88
|
const escapedPhase = escapeRegex(phaseNum);
|
|
22
89
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
);
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Fallback: check if phase exists in summary list but missing detail section
|
|
32
|
-
const checklistPattern = new RegExp(
|
|
33
|
-
`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${escapedPhase}:\\s*([^*]+)\\*\\*`,
|
|
34
|
-
'i'
|
|
35
|
-
);
|
|
36
|
-
const checklistMatch = content.match(checklistPattern);
|
|
37
|
-
|
|
38
|
-
if (checklistMatch) {
|
|
39
|
-
// Phase exists in summary but missing detail section - malformed ROADMAP
|
|
40
|
-
output({
|
|
41
|
-
found: false,
|
|
42
|
-
phase_number: phaseNum,
|
|
43
|
-
phase_name: checklistMatch[1].trim(),
|
|
44
|
-
error: 'malformed_roadmap',
|
|
45
|
-
message: `Phase ${phaseNum} exists in summary list but missing "### Phase ${phaseNum}:" detail section. ROADMAP.md needs both formats.`
|
|
46
|
-
}, raw, '');
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
90
|
+
// Search the current milestone slice first, then fall back to full roadmap.
|
|
91
|
+
// A malformed_roadmap result (checklist-only) from the milestone should not
|
|
92
|
+
// block finding a full header match in the wider roadmap content.
|
|
93
|
+
const fullContent = stripShippedMilestones(rawContent);
|
|
94
|
+
const milestoneResult = searchPhaseInContent(milestoneContent, escapedPhase, phaseNum);
|
|
95
|
+
const result = (milestoneResult && !milestoneResult.error)
|
|
96
|
+
? milestoneResult
|
|
97
|
+
: searchPhaseInContent(fullContent, escapedPhase, phaseNum) || milestoneResult;
|
|
49
98
|
|
|
99
|
+
if (!result) {
|
|
50
100
|
output({ found: false, phase_number: phaseNum }, raw, '');
|
|
51
101
|
return;
|
|
52
102
|
}
|
|
53
103
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const restOfContent = content.slice(headerIndex);
|
|
59
|
-
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
60
|
-
const sectionEnd = nextHeaderMatch
|
|
61
|
-
? headerIndex + nextHeaderMatch.index
|
|
62
|
-
: content.length;
|
|
63
|
-
|
|
64
|
-
const section = content.slice(headerIndex, sectionEnd).trim();
|
|
65
|
-
|
|
66
|
-
// Extract goal if present (supports both **Goal:** and **Goal**: formats)
|
|
67
|
-
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
68
|
-
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
69
|
-
|
|
70
|
-
// Extract success criteria as structured array
|
|
71
|
-
const criteriaMatch = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i);
|
|
72
|
-
const success_criteria = criteriaMatch
|
|
73
|
-
? criteriaMatch[1].trim().split('\n').map(line => line.replace(/^\s*\d+\.\s*/, '').trim()).filter(Boolean)
|
|
74
|
-
: [];
|
|
104
|
+
if (result.error) {
|
|
105
|
+
output(result, raw, '');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
75
108
|
|
|
76
|
-
output(
|
|
77
|
-
{
|
|
78
|
-
found: true,
|
|
79
|
-
phase_number: phaseNum,
|
|
80
|
-
phase_name: phaseName,
|
|
81
|
-
goal,
|
|
82
|
-
success_criteria,
|
|
83
|
-
section,
|
|
84
|
-
},
|
|
85
|
-
raw,
|
|
86
|
-
section
|
|
87
|
-
);
|
|
109
|
+
output(result, raw, result.section);
|
|
88
110
|
} catch (e) {
|
|
89
111
|
error('Failed to read ROADMAP.md: ' + e.message);
|
|
90
112
|
}
|
|
@@ -135,7 +157,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
135
157
|
try {
|
|
136
158
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
137
159
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
138
|
-
const dirMatch = dirs.find(d =>
|
|
160
|
+
const dirMatch = dirs.find(d => phaseTokenMatches(d, normalized));
|
|
139
161
|
|
|
140
162
|
if (dirMatch) {
|
|
141
163
|
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
|
|
@@ -254,64 +276,66 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
254
276
|
return;
|
|
255
277
|
}
|
|
256
278
|
|
|
257
|
-
|
|
258
|
-
|
|
279
|
+
// Wrap entire read-modify-write in lock to prevent concurrent corruption
|
|
280
|
+
withPlanningLock(cwd, () => {
|
|
281
|
+
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
282
|
+
const phaseEscaped = escapeRegex(phaseNum);
|
|
259
283
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
);
|
|
265
|
-
const dateField = isComplete ? ` ${today} ` : ' ';
|
|
266
|
-
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
|
267
|
-
const cells = fullRow.split('|').slice(1, -1); // drop leading/trailing empty from split
|
|
268
|
-
if (cells.length === 5) {
|
|
269
|
-
// 5-col: Phase | Milestone | Plans | Status | Completed
|
|
270
|
-
cells[2] = ` ${summaryCount}/${planCount} `;
|
|
271
|
-
cells[3] = ` ${status.padEnd(11)}`;
|
|
272
|
-
cells[4] = dateField;
|
|
273
|
-
} else if (cells.length === 4) {
|
|
274
|
-
// 4-col: Phase | Plans | Status | Completed
|
|
275
|
-
cells[1] = ` ${summaryCount}/${planCount} `;
|
|
276
|
-
cells[2] = ` ${status.padEnd(11)}`;
|
|
277
|
-
cells[3] = dateField;
|
|
278
|
-
}
|
|
279
|
-
return '|' + cells.join('|') + '|';
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
// Update plan count in phase detail section
|
|
283
|
-
const planCountPattern = new RegExp(
|
|
284
|
-
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
|
285
|
-
'i'
|
|
286
|
-
);
|
|
287
|
-
const planCountText = isComplete
|
|
288
|
-
? `${summaryCount}/${planCount} plans complete`
|
|
289
|
-
: `${summaryCount}/${planCount} plans executed`;
|
|
290
|
-
roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, `$1${planCountText}`);
|
|
291
|
-
|
|
292
|
-
// If complete: check checkbox
|
|
293
|
-
if (isComplete) {
|
|
294
|
-
const checkboxPattern = new RegExp(
|
|
295
|
-
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
|
|
296
|
-
'i'
|
|
284
|
+
// Progress table row: update Plans/Status/Date columns (handles 4 or 5 column tables)
|
|
285
|
+
const tableRowPattern = new RegExp(
|
|
286
|
+
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
|
|
287
|
+
'im'
|
|
297
288
|
);
|
|
298
|
-
|
|
299
|
-
|
|
289
|
+
const dateField = isComplete ? ` ${today} ` : ' ';
|
|
290
|
+
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
|
291
|
+
const cells = fullRow.split('|').slice(1, -1); // drop leading/trailing empty from split
|
|
292
|
+
if (cells.length === 5) {
|
|
293
|
+
// 5-col: Phase | Milestone | Plans | Status | Completed
|
|
294
|
+
cells[2] = ` ${summaryCount}/${planCount} `;
|
|
295
|
+
cells[3] = ` ${status.padEnd(11)}`;
|
|
296
|
+
cells[4] = dateField;
|
|
297
|
+
} else if (cells.length === 4) {
|
|
298
|
+
// 4-col: Phase | Plans | Status | Completed
|
|
299
|
+
cells[1] = ` ${summaryCount}/${planCount} `;
|
|
300
|
+
cells[2] = ` ${status.padEnd(11)}`;
|
|
301
|
+
cells[3] = dateField;
|
|
302
|
+
}
|
|
303
|
+
return '|' + cells.join('|') + '|';
|
|
304
|
+
});
|
|
300
305
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (!planId) continue;
|
|
305
|
-
const planEscaped = escapeRegex(planId);
|
|
306
|
-
const planCheckboxPattern = new RegExp(
|
|
307
|
-
`(-\\s*\\[) (\\]\\s*${planEscaped})`,
|
|
306
|
+
// Update plan count in phase detail section
|
|
307
|
+
const planCountPattern = new RegExp(
|
|
308
|
+
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
|
308
309
|
'i'
|
|
309
310
|
);
|
|
310
|
-
|
|
311
|
-
|
|
311
|
+
const planCountText = isComplete
|
|
312
|
+
? `${summaryCount}/${planCount} plans complete`
|
|
313
|
+
: `${summaryCount}/${planCount} plans executed`;
|
|
314
|
+
roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, `$1${planCountText}`);
|
|
315
|
+
|
|
316
|
+
// If complete: check checkbox
|
|
317
|
+
if (isComplete) {
|
|
318
|
+
const checkboxPattern = new RegExp(
|
|
319
|
+
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
|
|
320
|
+
'i'
|
|
321
|
+
);
|
|
322
|
+
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
|
|
323
|
+
}
|
|
312
324
|
|
|
313
|
-
|
|
325
|
+
// Mark completed plan checkboxes (e.g. "- [ ] 50-01-PLAN.md", "- [ ] 50-01:", or "- [ ] **50-01**")
|
|
326
|
+
for (const summaryFile of phaseInfo.summaries) {
|
|
327
|
+
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
328
|
+
if (!planId) continue;
|
|
329
|
+
const planEscaped = escapeRegex(planId);
|
|
330
|
+
const planCheckboxPattern = new RegExp(
|
|
331
|
+
`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`,
|
|
332
|
+
'i'
|
|
333
|
+
);
|
|
334
|
+
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
|
335
|
+
}
|
|
314
336
|
|
|
337
|
+
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
|
338
|
+
});
|
|
315
339
|
output({
|
|
316
340
|
updated: true,
|
|
317
341
|
phase: phaseNum,
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Drift Detection — Detects schema-relevant file changes and verifies
|
|
3
|
+
* that the appropriate database push command was executed during a phase.
|
|
4
|
+
*
|
|
5
|
+
* Prevents false-positive verification when schema files change but no push
|
|
6
|
+
* occurs — TypeScript types come from config, not the live database, so
|
|
7
|
+
* build/types pass on a broken state.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
// ─── ORM Patterns ────────────────────────────────────────────────────────────
|
|
13
|
+
//
|
|
14
|
+
// Each entry maps a glob-like pattern to an ORM name. Patterns use forward
|
|
15
|
+
// slashes internally — Windows backslash paths are normalized before matching.
|
|
16
|
+
|
|
17
|
+
const SCHEMA_PATTERNS = [
|
|
18
|
+
// Payload CMS
|
|
19
|
+
{ pattern: /^src\/collections\/.*\.ts$/, orm: 'payload' },
|
|
20
|
+
{ pattern: /^src\/globals\/.*\.ts$/, orm: 'payload' },
|
|
21
|
+
|
|
22
|
+
// Prisma
|
|
23
|
+
{ pattern: /^prisma\/schema\.prisma$/, orm: 'prisma' },
|
|
24
|
+
{ pattern: /^prisma\/schema\/.*\.prisma$/, orm: 'prisma' },
|
|
25
|
+
|
|
26
|
+
// Drizzle
|
|
27
|
+
{ pattern: /^drizzle\/schema\.ts$/, orm: 'drizzle' },
|
|
28
|
+
{ pattern: /^src\/db\/schema\.ts$/, orm: 'drizzle' },
|
|
29
|
+
{ pattern: /^drizzle\/.*\.ts$/, orm: 'drizzle' },
|
|
30
|
+
|
|
31
|
+
// Supabase
|
|
32
|
+
{ pattern: /^supabase\/migrations\/.*\.sql$/, orm: 'supabase' },
|
|
33
|
+
|
|
34
|
+
// TypeORM
|
|
35
|
+
{ pattern: /^src\/entities\/.*\.ts$/, orm: 'typeorm' },
|
|
36
|
+
{ pattern: /^src\/migrations\/.*\.ts$/, orm: 'typeorm' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// ─── Push Commands & Evidence Patterns ───────────────────────────────────────
|
|
40
|
+
//
|
|
41
|
+
// For each ORM, the push command that agents should run, plus regex patterns
|
|
42
|
+
// that indicate the push was actually executed (matched against execution logs,
|
|
43
|
+
// SUMMARY.md content, and git commit messages).
|
|
44
|
+
|
|
45
|
+
const ORM_INFO = {
|
|
46
|
+
payload: {
|
|
47
|
+
pushCommand: 'npx payload migrate',
|
|
48
|
+
envHint: 'CI=true PAYLOAD_MIGRATING=true npx payload migrate',
|
|
49
|
+
interactiveWarning: 'Payload migrate may require interactive prompts — use CI=true PAYLOAD_MIGRATING=true to suppress',
|
|
50
|
+
evidencePatterns: [
|
|
51
|
+
/payload\s+migrate/i,
|
|
52
|
+
/PAYLOAD_MIGRATING/,
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
prisma: {
|
|
56
|
+
pushCommand: 'npx prisma db push',
|
|
57
|
+
envHint: 'npx prisma db push --accept-data-loss (if destructive changes are intended)',
|
|
58
|
+
interactiveWarning: 'Prisma db push may prompt for confirmation on destructive changes — use --accept-data-loss to bypass',
|
|
59
|
+
evidencePatterns: [
|
|
60
|
+
/prisma\s+db\s+push/i,
|
|
61
|
+
/prisma\s+migrate\s+deploy/i,
|
|
62
|
+
/prisma\s+migrate\s+dev/i,
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
drizzle: {
|
|
66
|
+
pushCommand: 'npx drizzle-kit push',
|
|
67
|
+
envHint: 'npx drizzle-kit push',
|
|
68
|
+
interactiveWarning: null,
|
|
69
|
+
evidencePatterns: [
|
|
70
|
+
/drizzle-kit\s+push/i,
|
|
71
|
+
/drizzle-kit\s+migrate/i,
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
supabase: {
|
|
75
|
+
pushCommand: 'supabase db push',
|
|
76
|
+
envHint: 'supabase db push',
|
|
77
|
+
interactiveWarning: 'Supabase db push may require authentication — ensure SUPABASE_ACCESS_TOKEN is set',
|
|
78
|
+
evidencePatterns: [
|
|
79
|
+
/supabase\s+db\s+push/i,
|
|
80
|
+
/supabase\s+migration\s+up/i,
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
typeorm: {
|
|
84
|
+
pushCommand: 'npx typeorm migration:run',
|
|
85
|
+
envHint: 'npx typeorm migration:run -d src/data-source.ts',
|
|
86
|
+
interactiveWarning: null,
|
|
87
|
+
evidencePatterns: [
|
|
88
|
+
/typeorm\s+migration:run/i,
|
|
89
|
+
/typeorm\s+schema:sync/i,
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Detect schema-relevant files in a list of file paths.
|
|
98
|
+
*
|
|
99
|
+
* @param {string[]} files - List of file paths (relative to project root)
|
|
100
|
+
* @returns {{ detected: boolean, matches: string[], orms: string[] }}
|
|
101
|
+
*/
|
|
102
|
+
function detectSchemaFiles(files) {
|
|
103
|
+
const matches = [];
|
|
104
|
+
const orms = new Set();
|
|
105
|
+
|
|
106
|
+
for (const rawFile of files) {
|
|
107
|
+
// Normalize Windows backslash paths
|
|
108
|
+
const file = rawFile.replace(/\\/g, '/');
|
|
109
|
+
|
|
110
|
+
for (const { pattern, orm } of SCHEMA_PATTERNS) {
|
|
111
|
+
if (pattern.test(file)) {
|
|
112
|
+
matches.push(rawFile);
|
|
113
|
+
orms.add(orm);
|
|
114
|
+
break; // One match per file is enough
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
detected: matches.length > 0,
|
|
121
|
+
matches,
|
|
122
|
+
orms: Array.from(orms),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get ORM-specific push command info.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} ormName - ORM identifier (payload, prisma, drizzle, supabase, typeorm)
|
|
130
|
+
* @returns {{ pushCommand: string, envHint: string, interactiveWarning: string|null, evidencePatterns: RegExp[] } | null}
|
|
131
|
+
*/
|
|
132
|
+
function detectSchemaOrm(ormName) {
|
|
133
|
+
return ORM_INFO[ormName] || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check for schema drift: schema files changed but no push evidence found.
|
|
138
|
+
*
|
|
139
|
+
* @param {string[]} changedFiles - Files changed during the phase
|
|
140
|
+
* @param {string} executionLog - Combined text from SUMMARY.md, commit messages, and execution logs
|
|
141
|
+
* @param {{ skipCheck?: boolean }} [options] - Options
|
|
142
|
+
* @returns {{ driftDetected: boolean, blocking: boolean, schemaFiles: string[], orms: string[], unpushedOrms: string[], message: string, skipped?: boolean }}
|
|
143
|
+
*/
|
|
144
|
+
function checkSchemaDrift(changedFiles, executionLog, options = {}) {
|
|
145
|
+
const { skipCheck = false } = options;
|
|
146
|
+
|
|
147
|
+
const detection = detectSchemaFiles(changedFiles);
|
|
148
|
+
|
|
149
|
+
if (!detection.detected) {
|
|
150
|
+
return {
|
|
151
|
+
driftDetected: false,
|
|
152
|
+
blocking: false,
|
|
153
|
+
schemaFiles: [],
|
|
154
|
+
orms: [],
|
|
155
|
+
unpushedOrms: [],
|
|
156
|
+
message: '',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check which ORMs have push evidence in the execution log
|
|
161
|
+
const pushedOrms = new Set();
|
|
162
|
+
const unpushedOrms = [];
|
|
163
|
+
|
|
164
|
+
for (const orm of detection.orms) {
|
|
165
|
+
const info = ORM_INFO[orm];
|
|
166
|
+
if (!info) continue;
|
|
167
|
+
|
|
168
|
+
const hasPushEvidence = info.evidencePatterns.some(p => p.test(executionLog));
|
|
169
|
+
if (hasPushEvidence) {
|
|
170
|
+
pushedOrms.add(orm);
|
|
171
|
+
} else {
|
|
172
|
+
unpushedOrms.push(orm);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const driftDetected = unpushedOrms.length > 0;
|
|
177
|
+
|
|
178
|
+
if (!driftDetected) {
|
|
179
|
+
return {
|
|
180
|
+
driftDetected: false,
|
|
181
|
+
blocking: false,
|
|
182
|
+
schemaFiles: detection.matches,
|
|
183
|
+
orms: detection.orms,
|
|
184
|
+
unpushedOrms: [],
|
|
185
|
+
message: '',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Build actionable message
|
|
190
|
+
const pushCommands = unpushedOrms
|
|
191
|
+
.map(orm => {
|
|
192
|
+
const info = ORM_INFO[orm];
|
|
193
|
+
return info ? ` ${orm}: ${info.envHint || info.pushCommand}` : null;
|
|
194
|
+
})
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.join('\n');
|
|
197
|
+
|
|
198
|
+
const message = [
|
|
199
|
+
'Schema drift detected: schema-relevant files changed but no database push was executed.',
|
|
200
|
+
'',
|
|
201
|
+
`Schema files changed: ${detection.matches.join(', ')}`,
|
|
202
|
+
`ORMs requiring push: ${unpushedOrms.join(', ')}`,
|
|
203
|
+
'',
|
|
204
|
+
'Required push commands:',
|
|
205
|
+
pushCommands,
|
|
206
|
+
'',
|
|
207
|
+
'Run the appropriate push command, or set GSD_SKIP_SCHEMA_CHECK=true to bypass this gate.',
|
|
208
|
+
].join('\n');
|
|
209
|
+
|
|
210
|
+
if (skipCheck) {
|
|
211
|
+
return {
|
|
212
|
+
driftDetected: true,
|
|
213
|
+
blocking: false,
|
|
214
|
+
skipped: true,
|
|
215
|
+
schemaFiles: detection.matches,
|
|
216
|
+
orms: detection.orms,
|
|
217
|
+
unpushedOrms,
|
|
218
|
+
message: 'Schema drift detected but check was skipped (GSD_SKIP_SCHEMA_CHECK=true).',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
driftDetected: true,
|
|
224
|
+
blocking: true,
|
|
225
|
+
schemaFiles: detection.matches,
|
|
226
|
+
orms: detection.orms,
|
|
227
|
+
unpushedOrms,
|
|
228
|
+
message,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
SCHEMA_PATTERNS,
|
|
234
|
+
ORM_INFO,
|
|
235
|
+
detectSchemaFiles,
|
|
236
|
+
detectSchemaOrm,
|
|
237
|
+
checkSchemaDrift,
|
|
238
|
+
};
|
|
@@ -181,9 +181,11 @@ function scanForInjection(text, opts = {}) {
|
|
|
181
181
|
findings.push('Contains suspicious zero-width or invisible Unicode characters');
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
// Check for extremely long strings that could be prompt stuffing
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
// Check for extremely long strings that could be prompt stuffing.
|
|
185
|
+
// Normalize CRLF → LF before measuring so Windows checkouts don't inflate the count.
|
|
186
|
+
const normalizedLength = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').length;
|
|
187
|
+
if (normalizedLength > 50000) {
|
|
188
|
+
findings.push(`Suspicious text length: ${normalizedLength} chars (potential prompt stuffing)`);
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
|