gsd-opencode 1.33.2 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/gsd-advisor-researcher.md +23 -0
- package/agents/gsd-ai-researcher.md +142 -0
- package/agents/gsd-code-fixer.md +523 -0
- package/agents/gsd-code-reviewer.md +361 -0
- package/agents/gsd-debugger.md +14 -1
- package/agents/gsd-domain-researcher.md +162 -0
- package/agents/gsd-eval-auditor.md +170 -0
- package/agents/gsd-eval-planner.md +161 -0
- package/agents/gsd-executor.md +70 -7
- package/agents/gsd-framework-selector.md +167 -0
- package/agents/gsd-intel-updater.md +320 -0
- package/agents/gsd-phase-researcher.md +26 -0
- package/agents/gsd-plan-checker.md +12 -0
- package/agents/gsd-planner.md +16 -6
- package/agents/gsd-project-researcher.md +23 -0
- package/agents/gsd-ui-researcher.md +23 -0
- package/agents/gsd-verifier.md +55 -1
- package/commands/gsd/gsd-add-backlog.md +1 -1
- package/commands/gsd/gsd-add-phase.md +1 -1
- package/commands/gsd/gsd-add-todo.md +1 -1
- package/commands/gsd/gsd-ai-integration-phase.md +36 -0
- package/commands/gsd/gsd-audit-fix.md +33 -0
- package/commands/gsd/gsd-autonomous.md +1 -0
- package/commands/gsd/gsd-check-todos.md +1 -1
- package/commands/gsd/gsd-code-review-fix.md +52 -0
- package/commands/gsd/gsd-code-review.md +55 -0
- package/commands/gsd/gsd-complete-milestone.md +1 -1
- package/commands/gsd/gsd-debug.md +1 -1
- package/commands/gsd/gsd-eval-review.md +32 -0
- package/commands/gsd/gsd-explore.md +27 -0
- package/commands/gsd/gsd-from-gsd2.md +45 -0
- package/commands/gsd/gsd-health.md +1 -1
- package/commands/gsd/gsd-import.md +36 -0
- package/commands/gsd/gsd-insert-phase.md +1 -1
- package/commands/gsd/gsd-intel.md +183 -0
- package/commands/gsd/gsd-manager.md +1 -1
- package/commands/gsd/gsd-next.md +2 -0
- package/commands/gsd/gsd-reapply-patches.md +58 -3
- package/commands/gsd/gsd-remove-phase.md +1 -1
- package/commands/gsd/gsd-review.md +4 -2
- package/commands/gsd/gsd-scan.md +26 -0
- package/commands/gsd/gsd-set-profile.md +1 -1
- package/commands/gsd/gsd-thread.md +1 -1
- package/commands/gsd/gsd-undo.md +34 -0
- package/commands/gsd/gsd-workstreams.md +6 -6
- package/get-shit-done/bin/gsd-tools.cjs +143 -5
- package/get-shit-done/bin/lib/commands.cjs +10 -2
- package/get-shit-done/bin/lib/config.cjs +71 -37
- package/get-shit-done/bin/lib/core.cjs +70 -8
- package/get-shit-done/bin/lib/gsd2-import.cjs +511 -0
- package/get-shit-done/bin/lib/init.cjs +20 -6
- package/get-shit-done/bin/lib/intel.cjs +660 -0
- package/get-shit-done/bin/lib/learnings.cjs +378 -0
- package/get-shit-done/bin/lib/milestone.cjs +25 -15
- package/get-shit-done/bin/lib/model-profiles.cjs +17 -17
- package/get-shit-done/bin/lib/phase.cjs +148 -112
- package/get-shit-done/bin/lib/roadmap.cjs +12 -5
- package/get-shit-done/bin/lib/security.cjs +119 -0
- package/get-shit-done/bin/lib/state.cjs +283 -221
- package/get-shit-done/bin/lib/template.cjs +8 -4
- package/get-shit-done/bin/lib/verify.cjs +42 -5
- package/get-shit-done/references/ai-evals.md +156 -0
- package/get-shit-done/references/ai-frameworks.md +186 -0
- package/get-shit-done/references/common-bug-patterns.md +114 -0
- package/get-shit-done/references/few-shot-examples/plan-checker.md +73 -0
- package/get-shit-done/references/few-shot-examples/verifier.md +109 -0
- package/get-shit-done/references/gates.md +70 -0
- package/get-shit-done/references/ios-scaffold.md +123 -0
- package/get-shit-done/references/model-profile-resolution.md +6 -7
- package/get-shit-done/references/model-profiles.md +20 -14
- package/get-shit-done/references/planning-config.md +237 -0
- package/get-shit-done/references/thinking-models-debug.md +44 -0
- package/get-shit-done/references/thinking-models-execution.md +50 -0
- package/get-shit-done/references/thinking-models-planning.md +62 -0
- package/get-shit-done/references/thinking-models-research.md +50 -0
- package/get-shit-done/references/thinking-models-verification.md +55 -0
- package/get-shit-done/references/thinking-partner.md +96 -0
- package/get-shit-done/references/universal-anti-patterns.md +6 -1
- package/get-shit-done/references/verification-overrides.md +227 -0
- package/get-shit-done/templates/AI-SPEC.md +246 -0
- package/get-shit-done/workflows/add-tests.md +3 -0
- package/get-shit-done/workflows/add-todo.md +2 -0
- package/get-shit-done/workflows/ai-integration-phase.md +284 -0
- package/get-shit-done/workflows/audit-fix.md +154 -0
- package/get-shit-done/workflows/autonomous.md +33 -2
- package/get-shit-done/workflows/check-todos.md +2 -0
- package/get-shit-done/workflows/cleanup.md +2 -0
- package/get-shit-done/workflows/code-review-fix.md +497 -0
- package/get-shit-done/workflows/code-review.md +515 -0
- package/get-shit-done/workflows/complete-milestone.md +40 -15
- package/get-shit-done/workflows/diagnose-issues.md +1 -1
- package/get-shit-done/workflows/discovery-phase.md +3 -1
- package/get-shit-done/workflows/discuss-phase-assumptions.md +1 -1
- package/get-shit-done/workflows/discuss-phase.md +21 -7
- package/get-shit-done/workflows/do.md +2 -0
- package/get-shit-done/workflows/docs-update.md +2 -0
- package/get-shit-done/workflows/eval-review.md +155 -0
- package/get-shit-done/workflows/execute-phase.md +307 -57
- package/get-shit-done/workflows/execute-plan.md +64 -93
- package/get-shit-done/workflows/explore.md +136 -0
- package/get-shit-done/workflows/help.md +1 -1
- package/get-shit-done/workflows/import.md +273 -0
- package/get-shit-done/workflows/inbox.md +387 -0
- package/get-shit-done/workflows/manager.md +4 -10
- package/get-shit-done/workflows/new-milestone.md +3 -1
- package/get-shit-done/workflows/new-project.md +2 -0
- package/get-shit-done/workflows/new-workspace.md +2 -0
- package/get-shit-done/workflows/next.md +56 -0
- package/get-shit-done/workflows/note.md +2 -0
- package/get-shit-done/workflows/plan-phase.md +97 -17
- package/get-shit-done/workflows/plant-seed.md +3 -0
- package/get-shit-done/workflows/pr-branch.md +41 -13
- package/get-shit-done/workflows/profile-user.md +4 -2
- package/get-shit-done/workflows/quick.md +99 -4
- package/get-shit-done/workflows/remove-workspace.md +2 -0
- package/get-shit-done/workflows/review.md +53 -6
- package/get-shit-done/workflows/scan.md +98 -0
- package/get-shit-done/workflows/secure-phase.md +2 -0
- package/get-shit-done/workflows/settings.md +18 -3
- package/get-shit-done/workflows/ship.md +3 -0
- package/get-shit-done/workflows/ui-phase.md +10 -2
- package/get-shit-done/workflows/ui-review.md +2 -0
- package/get-shit-done/workflows/undo.md +314 -0
- package/get-shit-done/workflows/update.md +2 -0
- package/get-shit-done/workflows/validate-phase.md +2 -0
- package/get-shit-done/workflows/verify-phase.md +83 -0
- package/get-shit-done/workflows/verify-work.md +12 -1
- package/package.json +1 -1
- package/skills/gsd-code-review/SKILL.md +48 -0
- package/skills/gsd-code-review-fix/SKILL.md +44 -0
|
@@ -6,7 +6,7 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, planningDir, withPlanningLock, output, error, readSubdirectories, phaseTokenMatches } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
-
const { writeStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, updatePerformanceMetricsSection } = require('./state.cjs');
|
|
9
|
+
const { writeStateMd, readModifyWriteStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, updatePerformanceMetricsSection } = require('./state.cjs');
|
|
10
10
|
|
|
11
11
|
function cmdPhasesList(cwd, options, raw) {
|
|
12
12
|
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
@@ -88,50 +88,49 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
|
88
88
|
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
89
89
|
const normalized = normalizePhaseName(basePhase);
|
|
90
90
|
|
|
91
|
-
// Check if phases directory exists
|
|
92
|
-
if (!fs.existsSync(phasesDir)) {
|
|
93
|
-
output(
|
|
94
|
-
{
|
|
95
|
-
found: false,
|
|
96
|
-
base_phase: normalized,
|
|
97
|
-
next: `${normalized}.1`,
|
|
98
|
-
existing: [],
|
|
99
|
-
},
|
|
100
|
-
raw,
|
|
101
|
-
`${normalized}.1`
|
|
102
|
-
);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
91
|
try {
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
// Check if base phase exists
|
|
111
|
-
const baseExists = dirs.some(d => phaseTokenMatches(d, normalized));
|
|
92
|
+
let baseExists = false;
|
|
93
|
+
const decimalSet = new Set();
|
|
112
94
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
95
|
+
// Scan directory names for existing decimal phases
|
|
96
|
+
if (fs.existsSync(phasesDir)) {
|
|
97
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
98
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
99
|
+
baseExists = dirs.some(d => phaseTokenMatches(d, normalized));
|
|
116
100
|
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
const dirPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${escapeRegex(normalized)}\\.(\\d+)`);
|
|
102
|
+
for (const dir of dirs) {
|
|
103
|
+
const match = dir.match(dirPattern);
|
|
104
|
+
if (match) decimalSet.add(parseInt(match[1], 10));
|
|
121
105
|
}
|
|
122
106
|
}
|
|
123
107
|
|
|
124
|
-
//
|
|
125
|
-
|
|
108
|
+
// Also scan ROADMAP.md for phase entries that may not have directories yet
|
|
109
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
110
|
+
if (fs.existsSync(roadmapPath)) {
|
|
111
|
+
try {
|
|
112
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
113
|
+
const phasePattern = new RegExp(
|
|
114
|
+
`#{2,4}\\s*Phase\\s+0*${escapeRegex(normalized)}\\.(\\d+)\\s*:`, 'gi'
|
|
115
|
+
);
|
|
116
|
+
let pm;
|
|
117
|
+
while ((pm = phasePattern.exec(roadmapContent)) !== null) {
|
|
118
|
+
decimalSet.add(parseInt(pm[1], 10));
|
|
119
|
+
}
|
|
120
|
+
} catch { /* ROADMAP.md read failure is non-fatal */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Build sorted list of existing decimals
|
|
124
|
+
const existingDecimals = Array.from(decimalSet)
|
|
125
|
+
.sort((a, b) => a - b)
|
|
126
|
+
.map(n => `${normalized}.${n}`);
|
|
126
127
|
|
|
127
128
|
// Calculate next decimal
|
|
128
129
|
let nextDecimal;
|
|
129
|
-
if (
|
|
130
|
+
if (decimalSet.size === 0) {
|
|
130
131
|
nextDecimal = `${normalized}.1`;
|
|
131
132
|
} else {
|
|
132
|
-
|
|
133
|
-
const lastNum = parseInt(lastDecimal.split('.')[1], 10);
|
|
134
|
-
nextDecimal = `${normalized}.${lastNum + 1}`;
|
|
133
|
+
nextDecimal = `${normalized}.${Math.max(...decimalSet) + 1}`;
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
output(
|
|
@@ -341,15 +340,34 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
341
340
|
if (!_newPhaseId) error('--id required when phase_naming is "custom"');
|
|
342
341
|
_dirName = `${prefix}${_newPhaseId}-${slug}`;
|
|
343
342
|
} else {
|
|
344
|
-
// Sequential mode: find highest integer phase number
|
|
343
|
+
// Sequential mode: find highest integer phase number from two sources:
|
|
344
|
+
// 1. ROADMAP.md (current milestone only)
|
|
345
|
+
// 2. .planning/phases/ on disk (orphan directories not tracked in roadmap)
|
|
346
|
+
// Skip 999.x backlog phases — they live outside the active sequence
|
|
345
347
|
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
|
346
348
|
let maxPhase = 0;
|
|
347
349
|
let m;
|
|
348
350
|
while ((m = phasePattern.exec(content)) !== null) {
|
|
349
351
|
const num = parseInt(m[1], 10);
|
|
352
|
+
if (num >= 999) continue; // backlog phases use 999.x numbering
|
|
350
353
|
if (num > maxPhase) maxPhase = num;
|
|
351
354
|
}
|
|
352
355
|
|
|
356
|
+
// Also scan .planning/phases/ for orphan directories not tracked in ROADMAP.
|
|
357
|
+
// Directory names follow: [PREFIX-]NN-slug (e.g. 03-api or CK-05-old-feature).
|
|
358
|
+
// Strip the optional project_code prefix before extracting the leading integer.
|
|
359
|
+
const phasesOnDisk = path.join(planningDir(cwd), 'phases');
|
|
360
|
+
if (fs.existsSync(phasesOnDisk)) {
|
|
361
|
+
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
|
|
362
|
+
for (const entry of fs.readdirSync(phasesOnDisk)) {
|
|
363
|
+
const match = entry.match(dirNumPattern);
|
|
364
|
+
if (!match) continue;
|
|
365
|
+
const num = parseInt(match[1], 10);
|
|
366
|
+
if (num >= 999) continue; // skip backlog orphans
|
|
367
|
+
if (num > maxPhase) maxPhase = num;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
353
371
|
_newPhaseId = maxPhase + 1;
|
|
354
372
|
const paddedNum = String(_newPhaseId).padStart(2, '0');
|
|
355
373
|
_dirName = `${prefix}${paddedNum}-${slug}`;
|
|
@@ -416,22 +434,31 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
416
434
|
error(`Phase ${afterPhase} not found in ROADMAP.md`);
|
|
417
435
|
}
|
|
418
436
|
|
|
419
|
-
// Calculate next decimal
|
|
437
|
+
// Calculate next decimal by scanning both directories AND ROADMAP.md entries
|
|
420
438
|
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
421
439
|
const normalizedBase = normalizePhaseName(afterPhase);
|
|
422
|
-
|
|
440
|
+
const decimalSet = new Set();
|
|
423
441
|
|
|
424
442
|
try {
|
|
425
443
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
426
444
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
427
|
-
const decimalPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${normalizedBase}\\.(\\d+)`);
|
|
445
|
+
const decimalPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${escapeRegex(normalizedBase)}\\.(\\d+)`);
|
|
428
446
|
for (const dir of dirs) {
|
|
429
447
|
const dm = dir.match(decimalPattern);
|
|
430
|
-
if (dm)
|
|
448
|
+
if (dm) decimalSet.add(parseInt(dm[1], 10));
|
|
431
449
|
}
|
|
432
450
|
} catch { /* intentionally empty */ }
|
|
433
451
|
|
|
434
|
-
|
|
452
|
+
// Also scan ROADMAP.md content (already loaded) for decimal entries
|
|
453
|
+
const rmPhasePattern = new RegExp(
|
|
454
|
+
`#{2,4}\\s*Phase\\s+0*${escapeRegex(normalizedBase)}\\.(\\d+)\\s*:`, 'gi'
|
|
455
|
+
);
|
|
456
|
+
let rmMatch;
|
|
457
|
+
while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
|
|
458
|
+
decimalSet.add(parseInt(rmMatch[1], 10));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const nextDecimal = decimalSet.size === 0 ? 1 : Math.max(...decimalSet) + 1;
|
|
435
462
|
const _decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
|
436
463
|
// Optional project code prefix
|
|
437
464
|
const insertConfig = loadConfig(cwd);
|
|
@@ -624,19 +651,20 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
624
651
|
// Update ROADMAP.md
|
|
625
652
|
updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10), cwd);
|
|
626
653
|
|
|
627
|
-
// Update STATE.md phase count
|
|
654
|
+
// Update STATE.md phase count atomically (#P4.4)
|
|
628
655
|
const statePath = path.join(planningDir(cwd), 'STATE.md');
|
|
629
656
|
if (fs.existsSync(statePath)) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
657
|
+
readModifyWriteStateMd(statePath, (stateContent) => {
|
|
658
|
+
const totalRaw = stateExtractField(stateContent, 'Total Phases');
|
|
659
|
+
if (totalRaw) {
|
|
660
|
+
stateContent = stateReplaceField(stateContent, 'Total Phases', String(parseInt(totalRaw, 10) - 1)) || stateContent;
|
|
661
|
+
}
|
|
662
|
+
const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
|
|
663
|
+
if (ofMatch) {
|
|
664
|
+
stateContent = stateContent.replace(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
|
|
665
|
+
}
|
|
666
|
+
return stateContent;
|
|
667
|
+
}, cwd);
|
|
640
668
|
}
|
|
641
669
|
|
|
642
670
|
output({
|
|
@@ -701,7 +729,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
701
729
|
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
|
|
702
730
|
'i'
|
|
703
731
|
);
|
|
704
|
-
roadmapContent =
|
|
732
|
+
roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
|
|
705
733
|
|
|
706
734
|
// Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
|
|
707
735
|
const phaseEscaped = escapeRegex(phaseNum);
|
|
@@ -725,13 +753,20 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
725
753
|
return '|' + cells.join('|') + '|';
|
|
726
754
|
});
|
|
727
755
|
|
|
728
|
-
// Update plan count in phase section
|
|
756
|
+
// Update plan count in phase section.
|
|
757
|
+
// Use direct .replace() rather than replaceInCurrentMilestone() so this
|
|
758
|
+
// works when the current milestone section is itself inside a <details>
|
|
759
|
+
// block (the standard /gsd-new-project layout). replaceInCurrentMilestone
|
|
760
|
+
// scopes to content after the last </details>, which misses content inside
|
|
761
|
+
// the current milestone's own <details> wrapper (#2005).
|
|
762
|
+
// The phase-scoped heading pattern is specific enough to avoid matching
|
|
763
|
+
// archived phases (which belong to different milestones).
|
|
729
764
|
const planCountPattern = new RegExp(
|
|
730
765
|
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
|
731
766
|
'i'
|
|
732
767
|
);
|
|
733
|
-
roadmapContent =
|
|
734
|
-
|
|
768
|
+
roadmapContent = roadmapContent.replace(
|
|
769
|
+
planCountPattern,
|
|
735
770
|
`$1${summaryCount}/${planCount} plans complete`
|
|
736
771
|
);
|
|
737
772
|
|
|
@@ -834,71 +869,72 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
834
869
|
} catch { /* intentionally empty */ }
|
|
835
870
|
}
|
|
836
871
|
|
|
837
|
-
// Update STATE.md —
|
|
872
|
+
// Update STATE.md atomically — hold lock across read-modify-write (#P4.4).
|
|
873
|
+
// Previously read outside the lock; a crash between the ROADMAP update
|
|
874
|
+
// (locked above) and this write left ROADMAP/STATE inconsistent.
|
|
838
875
|
if (fs.existsSync(statePath)) {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
876
|
+
readModifyWriteStateMd(statePath, (stateContent) => {
|
|
877
|
+
// Update Current Phase — preserve "X of Y (Name)" compound format
|
|
878
|
+
const phaseValue = nextPhaseNum || phaseNum;
|
|
879
|
+
const existingPhaseField = stateExtractField(stateContent, 'Current Phase')
|
|
880
|
+
|| stateExtractField(stateContent, 'Phase');
|
|
881
|
+
let newPhaseValue = String(phaseValue);
|
|
882
|
+
if (existingPhaseField) {
|
|
883
|
+
const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
|
|
884
|
+
const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
|
|
885
|
+
if (totalMatch) {
|
|
886
|
+
const total = totalMatch[1];
|
|
887
|
+
const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : '');
|
|
888
|
+
newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
|
|
889
|
+
}
|
|
853
890
|
}
|
|
854
|
-
|
|
855
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
|
|
891
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
|
|
856
892
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
// Update Status
|
|
863
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
|
|
864
|
-
isLastPhase ? 'Milestone complete' : 'Ready to plan');
|
|
865
|
-
|
|
866
|
-
// Update Current Plan
|
|
867
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
|
|
868
|
-
|
|
869
|
-
// Update Last Activity
|
|
870
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
|
|
871
|
-
|
|
872
|
-
// Update Last Activity Description
|
|
873
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
|
|
874
|
-
`Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);
|
|
875
|
-
|
|
876
|
-
// Increment Completed Phases counter (#956)
|
|
877
|
-
const completedRaw = stateExtractField(stateContent, 'Completed Phases');
|
|
878
|
-
if (completedRaw) {
|
|
879
|
-
const newCompleted = parseInt(completedRaw, 10) + 1;
|
|
880
|
-
stateContent = stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) || stateContent;
|
|
893
|
+
// Update Current Phase Name
|
|
894
|
+
if (nextPhaseName) {
|
|
895
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
|
|
896
|
+
}
|
|
881
897
|
|
|
882
|
-
//
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
898
|
+
// Update Status
|
|
899
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
|
|
900
|
+
isLastPhase ? 'Milestone complete' : 'Ready to plan');
|
|
901
|
+
|
|
902
|
+
// Update Current Plan
|
|
903
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
|
|
904
|
+
|
|
905
|
+
// Update Last Activity
|
|
906
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
|
|
907
|
+
|
|
908
|
+
// Update Last Activity Description
|
|
909
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
|
|
910
|
+
`Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);
|
|
911
|
+
|
|
912
|
+
// Increment Completed Phases counter (#956)
|
|
913
|
+
const completedRaw = stateExtractField(stateContent, 'Completed Phases');
|
|
914
|
+
if (completedRaw) {
|
|
915
|
+
const newCompleted = parseInt(completedRaw, 10) + 1;
|
|
916
|
+
stateContent = stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) || stateContent;
|
|
917
|
+
|
|
918
|
+
// Recalculate percent based on completed / total (#956)
|
|
919
|
+
const totalRaw = stateExtractField(stateContent, 'Total Phases');
|
|
920
|
+
if (totalRaw) {
|
|
921
|
+
const totalPhases = parseInt(totalRaw, 10);
|
|
922
|
+
if (totalPhases > 0) {
|
|
923
|
+
const newPercent = Math.round((newCompleted / totalPhases) * 100);
|
|
924
|
+
stateContent = stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
|
|
925
|
+
stateContent = stateContent.replace(
|
|
926
|
+
/(percent:\s*)\d+/,
|
|
927
|
+
`$1${newPercent}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
894
930
|
}
|
|
895
931
|
}
|
|
896
|
-
}
|
|
897
932
|
|
|
898
|
-
|
|
899
|
-
|
|
933
|
+
// Gate 4: Update Performance Metrics section (#1627)
|
|
934
|
+
stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
|
|
900
935
|
|
|
901
|
-
|
|
936
|
+
return stateContent;
|
|
937
|
+
}, cwd);
|
|
902
938
|
}
|
|
903
939
|
|
|
904
940
|
const result = {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { escapeRegex, normalizePhaseName, planningPaths, withPlanningLock, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, phaseTokenMatches } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, normalizePhaseName, planningPaths, withPlanningLock, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, phaseTokenMatches, atomicWriteFileSync } = require('./core.cjs');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Search for a phase header (and its section) within the given content string.
|
|
@@ -129,6 +129,15 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
129
129
|
const phases = [];
|
|
130
130
|
let match;
|
|
131
131
|
|
|
132
|
+
// Build phase directory lookup once (O(1) readdir instead of O(N) per phase)
|
|
133
|
+
const _phaseDirNames = (() => {
|
|
134
|
+
try {
|
|
135
|
+
return fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
136
|
+
.filter(e => e.isDirectory())
|
|
137
|
+
.map(e => e.name);
|
|
138
|
+
} catch { return []; }
|
|
139
|
+
})();
|
|
140
|
+
|
|
132
141
|
while ((match = phasePattern.exec(content)) !== null) {
|
|
133
142
|
const phaseNum = match[1];
|
|
134
143
|
const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
|
|
@@ -155,9 +164,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
155
164
|
let hasResearch = false;
|
|
156
165
|
|
|
157
166
|
try {
|
|
158
|
-
const
|
|
159
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
160
|
-
const dirMatch = dirs.find(d => phaseTokenMatches(d, normalized));
|
|
167
|
+
const dirMatch = _phaseDirNames.find(d => phaseTokenMatches(d, normalized));
|
|
161
168
|
|
|
162
169
|
if (dirMatch) {
|
|
163
170
|
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
|
|
@@ -334,7 +341,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
334
341
|
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
|
335
342
|
}
|
|
336
343
|
|
|
337
|
-
|
|
344
|
+
atomicWriteFileSync(roadmapPath, roadmapContent, 'utf-8');
|
|
338
345
|
});
|
|
339
346
|
output({
|
|
340
347
|
updated: true,
|
|
@@ -152,6 +152,25 @@ const INJECTION_PATTERNS = [
|
|
|
152
152
|
/(?:run|execute|call|invoke)\s+(?:the\s+)?(?:bash|shell|exec|spawn)\s+(?:tool|command)/i,
|
|
153
153
|
];
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Layer 2: Encoding-obfuscation patterns with custom finding messages.
|
|
157
|
+
* Each entry: { pattern: RegExp, message: string }
|
|
158
|
+
*/
|
|
159
|
+
const OBFUSCATION_PATTERN_ENTRIES = [
|
|
160
|
+
{
|
|
161
|
+
pattern: /\b(\w\s){4,}\w\b/,
|
|
162
|
+
message: 'Character-spacing obfuscation pattern detected (e.g. "i g n o r e")',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
pattern: /<\/?(system|human|assistant|user)\s*>/i,
|
|
166
|
+
message: 'Delimiter injection pattern: <system>/<assistant>/<user> tag detected',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
pattern: /0x[0-9a-fA-F]{16,}/,
|
|
170
|
+
message: 'Long hex sequence detected — possible encoded payload',
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
155
174
|
/**
|
|
156
175
|
* Scan text for potential prompt injection patterns.
|
|
157
176
|
* Returns an array of findings (empty = clean).
|
|
@@ -174,6 +193,13 @@ function scanForInjection(text, opts = {}) {
|
|
|
174
193
|
}
|
|
175
194
|
}
|
|
176
195
|
|
|
196
|
+
// Layer 2: encoding-obfuscation patterns with custom messages
|
|
197
|
+
for (const entry of OBFUSCATION_PATTERN_ENTRIES) {
|
|
198
|
+
if (entry.pattern.test(text)) {
|
|
199
|
+
findings.push(entry.message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
177
203
|
if (opts.strict) {
|
|
178
204
|
// Check for suspicious Unicode that could hide instructions
|
|
179
205
|
// (zero-width chars, RTL override, homoglyph attacks)
|
|
@@ -181,6 +207,12 @@ function scanForInjection(text, opts = {}) {
|
|
|
181
207
|
findings.push('Contains suspicious zero-width or invisible Unicode characters');
|
|
182
208
|
}
|
|
183
209
|
|
|
210
|
+
// Layer 1: Unicode tag block U+E0000–U+E007F (2025 supply-chain attack vector)
|
|
211
|
+
// These characters are invisible and can embed hidden instructions
|
|
212
|
+
if (/[\uDB40\uDC00-\uDB40\uDC7F]/u.test(text) || /[\u{E0000}-\u{E007F}]/u.test(text)) {
|
|
213
|
+
findings.push('Contains Unicode tag block characters (U+E0000–E007F) — invisible instruction injection vector');
|
|
214
|
+
}
|
|
215
|
+
|
|
184
216
|
// Check for extremely long strings that could be prompt stuffing.
|
|
185
217
|
// Normalize CRLF → LF before measuring so Windows checkouts don't inflate the count.
|
|
186
218
|
const normalizedLength = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').length;
|
|
@@ -361,6 +393,87 @@ function validateFieldName(field) {
|
|
|
361
393
|
return { valid: false, error: `Invalid field name: "${field}"` };
|
|
362
394
|
}
|
|
363
395
|
|
|
396
|
+
// ─── Layer 3: Structural Schema Validation ───────────────────────────────────
|
|
397
|
+
|
|
398
|
+
const KNOWN_VALID_TAGS = new Set([
|
|
399
|
+
'objective', 'process', 'step', 'success_criteria', 'critical_rules',
|
|
400
|
+
'available_agent_types', 'purpose', 'required_reading',
|
|
401
|
+
]);
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Validate the XML structure of a prompt file.
|
|
405
|
+
* For agent/workflow files, flags any XML tag not in the known-valid set.
|
|
406
|
+
*
|
|
407
|
+
* @param {string} text - The file content to validate
|
|
408
|
+
* @param {'agent'|'workflow'|'unknown'} fileType - The type of prompt file
|
|
409
|
+
* @returns {{ valid: boolean, violations: string[] }}
|
|
410
|
+
*/
|
|
411
|
+
function validatePromptStructure(text, fileType) {
|
|
412
|
+
if (!text || typeof text !== 'string') {
|
|
413
|
+
return { valid: true, violations: [] };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (fileType !== 'agent' && fileType !== 'workflow') {
|
|
417
|
+
return { valid: true, violations: [] };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const violations = [];
|
|
421
|
+
const tagRegex = /<([A-Za-z][A-Za-z0-9_-]*)/g;
|
|
422
|
+
let match;
|
|
423
|
+
while ((match = tagRegex.exec(text)) !== null) {
|
|
424
|
+
const tag = match[1].toLowerCase();
|
|
425
|
+
if (!KNOWN_VALID_TAGS.has(tag)) {
|
|
426
|
+
violations.push(`Unknown XML tag in ${fileType} file: <${tag}>`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return { valid: violations.length === 0, violations };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ─── Layer 4: Paragraph-Level Entropy Anomaly Detection ─────────────────────
|
|
434
|
+
|
|
435
|
+
function shannonEntropy(text) {
|
|
436
|
+
if (!text || text.length === 0) return 0;
|
|
437
|
+
const freq = {};
|
|
438
|
+
for (const ch of text) {
|
|
439
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
440
|
+
}
|
|
441
|
+
const len = text.length;
|
|
442
|
+
let entropy = 0;
|
|
443
|
+
for (const count of Object.values(freq)) {
|
|
444
|
+
const p = count / len;
|
|
445
|
+
entropy -= p * Math.log2(p);
|
|
446
|
+
}
|
|
447
|
+
return entropy;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Scan text for paragraphs with anomalously high Shannon entropy.
|
|
452
|
+
*
|
|
453
|
+
* @param {string} text - The text to scan
|
|
454
|
+
* @returns {{ clean: boolean, findings: string[] }}
|
|
455
|
+
*/
|
|
456
|
+
function scanEntropyAnomalies(text) {
|
|
457
|
+
if (!text || typeof text !== 'string') {
|
|
458
|
+
return { clean: true, findings: [] };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const findings = [];
|
|
462
|
+
const paragraphs = text.split(/\n\n+/);
|
|
463
|
+
|
|
464
|
+
for (const para of paragraphs) {
|
|
465
|
+
if (para.length <= 50) continue;
|
|
466
|
+
const entropy = shannonEntropy(para);
|
|
467
|
+
if (entropy > 5.5) {
|
|
468
|
+
findings.push(
|
|
469
|
+
`High-entropy paragraph detected (${entropy.toFixed(2)} bits/char) — possible encoded payload`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return { clean: findings.length === 0, findings };
|
|
475
|
+
}
|
|
476
|
+
|
|
364
477
|
module.exports = {
|
|
365
478
|
// Path safety
|
|
366
479
|
validatePath,
|
|
@@ -381,4 +494,10 @@ module.exports = {
|
|
|
381
494
|
// Input validation
|
|
382
495
|
validatePhaseNumber,
|
|
383
496
|
validateFieldName,
|
|
497
|
+
|
|
498
|
+
// Structural validation (Layer 3)
|
|
499
|
+
validatePromptStructure,
|
|
500
|
+
|
|
501
|
+
// Entropy anomaly detection (Layer 4)
|
|
502
|
+
scanEntropyAnomalies,
|
|
384
503
|
};
|