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
|
@@ -870,6 +870,23 @@ function cmdInitManager(cwd, raw) {
|
|
|
870
870
|
const phasesDir = paths.phases;
|
|
871
871
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
872
872
|
|
|
873
|
+
// Pre-compute directory listing once (avoids O(N) readdirSync per phase)
|
|
874
|
+
const _phaseDirEntries = (() => {
|
|
875
|
+
try {
|
|
876
|
+
return fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
877
|
+
.filter(e => e.isDirectory())
|
|
878
|
+
.map(e => e.name);
|
|
879
|
+
} catch { return []; }
|
|
880
|
+
})();
|
|
881
|
+
|
|
882
|
+
// Pre-extract all checkbox states in a single pass (avoids O(N) regex per phase)
|
|
883
|
+
const _checkboxStates = new Map();
|
|
884
|
+
const _cbPattern = /-\s*\[(x| )\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi;
|
|
885
|
+
let _cbMatch;
|
|
886
|
+
while ((_cbMatch = _cbPattern.exec(content)) !== null) {
|
|
887
|
+
_checkboxStates.set(_cbMatch[2], _cbMatch[1].toLowerCase() === 'x');
|
|
888
|
+
}
|
|
889
|
+
|
|
873
890
|
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
874
891
|
const phases = [];
|
|
875
892
|
let match;
|
|
@@ -900,8 +917,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
900
917
|
let isActive = false;
|
|
901
918
|
|
|
902
919
|
try {
|
|
903
|
-
const
|
|
904
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).filter(isDirInMilestone);
|
|
920
|
+
const dirs = _phaseDirEntries.filter(isDirInMilestone);
|
|
905
921
|
const dirMatch = dirs.find(d => phaseTokenMatches(d, normalized));
|
|
906
922
|
|
|
907
923
|
if (dirMatch) {
|
|
@@ -935,10 +951,8 @@ function cmdInitManager(cwd, raw) {
|
|
|
935
951
|
}
|
|
936
952
|
} catch { /* intentionally empty */ }
|
|
937
953
|
|
|
938
|
-
// Check ROADMAP checkbox status
|
|
939
|
-
const
|
|
940
|
-
const checkboxMatch = content.match(checkboxPattern);
|
|
941
|
-
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
|
954
|
+
// Check ROADMAP checkbox status (pre-extracted above the loop)
|
|
955
|
+
const roadmapComplete = _checkboxStates.get(phaseNum) || false;
|
|
942
956
|
if (roadmapComplete && diskStatus !== 'complete') {
|
|
943
957
|
diskStatus = 'complete';
|
|
944
958
|
}
|
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intel.cjs -- Intel storage and query operations for GSD.
|
|
3
|
+
*
|
|
4
|
+
* Provides a persistent, queryable intelligence system for project metadata.
|
|
5
|
+
* Intel files live in .planning/intel/ and store structured data about
|
|
6
|
+
* the project's files, APIs, dependencies, architecture, and tech stack.
|
|
7
|
+
*
|
|
8
|
+
* All public functions gate on intel.enabled config (no-op when false).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
|
|
17
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const INTEL_DIR = '.planning/intel';
|
|
20
|
+
|
|
21
|
+
const INTEL_FILES = {
|
|
22
|
+
files: 'files.json',
|
|
23
|
+
apis: 'apis.json',
|
|
24
|
+
deps: 'deps.json',
|
|
25
|
+
arch: 'arch.md',
|
|
26
|
+
stack: 'stack.json'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ensure the intel directory exists under the given planning dir.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} planningDir - Path to .planning directory
|
|
35
|
+
* @returns {string} Full path to .planning/intel/
|
|
36
|
+
*/
|
|
37
|
+
function ensureIntelDir(planningDir) {
|
|
38
|
+
const intelPath = path.join(planningDir, 'intel');
|
|
39
|
+
if (!fs.existsSync(intelPath)) {
|
|
40
|
+
fs.mkdirSync(intelPath, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
return intelPath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check whether intel is enabled in the project config.
|
|
47
|
+
* Reads config.json directly via fs. Returns false by default
|
|
48
|
+
* (when no config, no intel key, or on error).
|
|
49
|
+
*
|
|
50
|
+
* @param {string} planningDir - Path to .planning directory
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
function isIntelEnabled(planningDir) {
|
|
54
|
+
try {
|
|
55
|
+
const configPath = path.join(planningDir, 'config.json');
|
|
56
|
+
if (!fs.existsSync(configPath)) return false;
|
|
57
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
58
|
+
if (config && config.intel && config.intel.enabled === true) return true;
|
|
59
|
+
return false;
|
|
60
|
+
} catch (_e) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return the standard disabled response object.
|
|
67
|
+
* @returns {{ disabled: true, message: string }}
|
|
68
|
+
*/
|
|
69
|
+
function disabledResponse() {
|
|
70
|
+
return { disabled: true, message: 'Intel system disabled. Set intel.enabled=true in config.json to activate.' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve full path to an intel file.
|
|
75
|
+
* @param {string} planningDir
|
|
76
|
+
* @param {string} filename
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
function intelFilePath(planningDir, filename) {
|
|
80
|
+
return path.join(planningDir, 'intel', filename);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Safely read and parse a JSON intel file.
|
|
85
|
+
* Returns null if file doesn't exist or can't be parsed.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} filePath
|
|
88
|
+
* @returns {object|null}
|
|
89
|
+
*/
|
|
90
|
+
function safeReadJson(filePath) {
|
|
91
|
+
try {
|
|
92
|
+
if (!fs.existsSync(filePath)) return null;
|
|
93
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
94
|
+
} catch (_e) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Compute SHA-256 hash of a file's contents.
|
|
101
|
+
* Returns null if the file doesn't exist.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} filePath
|
|
104
|
+
* @returns {string|null}
|
|
105
|
+
*/
|
|
106
|
+
function hashFile(filePath) {
|
|
107
|
+
try {
|
|
108
|
+
if (!fs.existsSync(filePath)) return null;
|
|
109
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
110
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
111
|
+
} catch (_e) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Search for a term (case-insensitive) in a JSON object's keys and string values.
|
|
118
|
+
* Returns an array of matching entries.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} data - The JSON data (expects { _meta, entries } or flat object)
|
|
121
|
+
* @param {string} term - Search term
|
|
122
|
+
* @returns {Array<{ key: string, value: * }>}
|
|
123
|
+
*/
|
|
124
|
+
function searchJsonEntries(data, term) {
|
|
125
|
+
if (!data || typeof data !== 'object') return [];
|
|
126
|
+
|
|
127
|
+
const entries = data.entries || data;
|
|
128
|
+
if (!entries || typeof entries !== 'object') return [];
|
|
129
|
+
|
|
130
|
+
const lowerTerm = term.toLowerCase();
|
|
131
|
+
const matches = [];
|
|
132
|
+
|
|
133
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
134
|
+
if (key === '_meta') continue;
|
|
135
|
+
|
|
136
|
+
// Check key match
|
|
137
|
+
if (key.toLowerCase().includes(lowerTerm)) {
|
|
138
|
+
matches.push({ key, value });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check string value match (recursive for objects)
|
|
143
|
+
if (matchesInValue(value, lowerTerm)) {
|
|
144
|
+
matches.push({ key, value });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return matches;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Recursively check if a term appears in any string value.
|
|
153
|
+
*
|
|
154
|
+
* @param {*} value
|
|
155
|
+
* @param {string} lowerTerm
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
function matchesInValue(value, lowerTerm) {
|
|
159
|
+
if (typeof value === 'string') {
|
|
160
|
+
return value.toLowerCase().includes(lowerTerm);
|
|
161
|
+
}
|
|
162
|
+
if (Array.isArray(value)) {
|
|
163
|
+
return value.some(v => matchesInValue(v, lowerTerm));
|
|
164
|
+
}
|
|
165
|
+
if (value && typeof value === 'object') {
|
|
166
|
+
return Object.values(value).some(v => matchesInValue(v, lowerTerm));
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Search for a term in arch.md text content.
|
|
173
|
+
* Returns matching lines.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} filePath - Path to arch.md
|
|
176
|
+
* @param {string} term - Search term
|
|
177
|
+
* @returns {string[]}
|
|
178
|
+
*/
|
|
179
|
+
function searchArchMd(filePath, term) {
|
|
180
|
+
try {
|
|
181
|
+
if (!fs.existsSync(filePath)) return [];
|
|
182
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
183
|
+
const lowerTerm = term.toLowerCase();
|
|
184
|
+
const lines = content.split(/\r?\n/);
|
|
185
|
+
return lines.filter(line => line.toLowerCase().includes(lowerTerm));
|
|
186
|
+
} catch (_e) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Query intel files for a search term.
|
|
195
|
+
* Searches across all JSON intel files (keys and values) and arch.md (text lines).
|
|
196
|
+
*
|
|
197
|
+
* @param {string} term - Search term (case-insensitive)
|
|
198
|
+
* @param {string} planningDir - Path to .planning directory
|
|
199
|
+
* @returns {{ matches: Array<{ source: string, entries: Array }>, term: string, total: number } | { disabled: true, message: string }}
|
|
200
|
+
*/
|
|
201
|
+
function intelQuery(term, planningDir) {
|
|
202
|
+
if (!isIntelEnabled(planningDir)) return disabledResponse();
|
|
203
|
+
|
|
204
|
+
const matches = [];
|
|
205
|
+
let total = 0;
|
|
206
|
+
|
|
207
|
+
// Search JSON intel files
|
|
208
|
+
for (const [_key, filename] of Object.entries(INTEL_FILES)) {
|
|
209
|
+
if (filename.endsWith('.md')) continue; // Skip arch.md here
|
|
210
|
+
|
|
211
|
+
const filePath = intelFilePath(planningDir, filename);
|
|
212
|
+
const data = safeReadJson(filePath);
|
|
213
|
+
if (!data) continue;
|
|
214
|
+
|
|
215
|
+
const found = searchJsonEntries(data, term);
|
|
216
|
+
if (found.length > 0) {
|
|
217
|
+
matches.push({ source: filename, entries: found });
|
|
218
|
+
total += found.length;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Search arch.md
|
|
223
|
+
const archPath = intelFilePath(planningDir, INTEL_FILES.arch);
|
|
224
|
+
const archMatches = searchArchMd(archPath, term);
|
|
225
|
+
if (archMatches.length > 0) {
|
|
226
|
+
matches.push({ source: INTEL_FILES.arch, entries: archMatches });
|
|
227
|
+
total += archMatches.length;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { matches, term, total };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Report status and staleness of each intel file.
|
|
235
|
+
* A file is considered stale if its updated_at is older than 24 hours.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} planningDir - Path to .planning directory
|
|
238
|
+
* @returns {{ files: object, overall_stale: boolean } | { disabled: true, message: string }}
|
|
239
|
+
*/
|
|
240
|
+
function intelStatus(planningDir) {
|
|
241
|
+
if (!isIntelEnabled(planningDir)) return disabledResponse();
|
|
242
|
+
|
|
243
|
+
const STALE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
const files = {};
|
|
246
|
+
let overallStale = false;
|
|
247
|
+
|
|
248
|
+
for (const [_key, filename] of Object.entries(INTEL_FILES)) {
|
|
249
|
+
const filePath = intelFilePath(planningDir, filename);
|
|
250
|
+
const exists = fs.existsSync(filePath);
|
|
251
|
+
|
|
252
|
+
if (!exists) {
|
|
253
|
+
files[filename] = { exists: false, updated_at: null, stale: true };
|
|
254
|
+
overallStale = true;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let updatedAt = null;
|
|
259
|
+
|
|
260
|
+
if (filename.endsWith('.md')) {
|
|
261
|
+
// For arch.md, use file mtime
|
|
262
|
+
try {
|
|
263
|
+
const stat = fs.statSync(filePath);
|
|
264
|
+
updatedAt = stat.mtime.toISOString();
|
|
265
|
+
} catch (_e) {
|
|
266
|
+
// intentionally silent: fall through on error
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
// For JSON files, read _meta.updated_at
|
|
270
|
+
const data = safeReadJson(filePath);
|
|
271
|
+
if (data && data._meta && data._meta.updated_at) {
|
|
272
|
+
updatedAt = data._meta.updated_at;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let stale = true;
|
|
277
|
+
if (updatedAt) {
|
|
278
|
+
const age = now - new Date(updatedAt).getTime();
|
|
279
|
+
stale = age > STALE_MS;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (stale) overallStale = true;
|
|
283
|
+
files[filename] = { exists: true, updated_at: updatedAt, stale };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { files, overall_stale: overallStale };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Show changes since the last full refresh by comparing file hashes.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} planningDir - Path to .planning directory
|
|
293
|
+
* @returns {{ changed: string[], added: string[], removed: string[] } | { no_baseline: true } | { disabled: true, message: string }}
|
|
294
|
+
*/
|
|
295
|
+
function intelDiff(planningDir) {
|
|
296
|
+
if (!isIntelEnabled(planningDir)) return disabledResponse();
|
|
297
|
+
|
|
298
|
+
const snapshotPath = intelFilePath(planningDir, '.last-refresh.json');
|
|
299
|
+
const snapshot = safeReadJson(snapshotPath);
|
|
300
|
+
|
|
301
|
+
if (!snapshot) {
|
|
302
|
+
return { no_baseline: true };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const prevHashes = snapshot.hashes || {};
|
|
306
|
+
const changed = [];
|
|
307
|
+
const added = [];
|
|
308
|
+
const removed = [];
|
|
309
|
+
|
|
310
|
+
// Check current files against snapshot
|
|
311
|
+
for (const [_key, filename] of Object.entries(INTEL_FILES)) {
|
|
312
|
+
const filePath = intelFilePath(planningDir, filename);
|
|
313
|
+
const currentHash = hashFile(filePath);
|
|
314
|
+
|
|
315
|
+
if (currentHash && !prevHashes[filename]) {
|
|
316
|
+
added.push(filename);
|
|
317
|
+
} else if (currentHash && prevHashes[filename] && currentHash !== prevHashes[filename]) {
|
|
318
|
+
changed.push(filename);
|
|
319
|
+
} else if (!currentHash && prevHashes[filename]) {
|
|
320
|
+
removed.push(filename);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { changed, added, removed };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Stub for triggering an intel update.
|
|
329
|
+
* The actual update is performed by the intel-updater agent (PLAN-02).
|
|
330
|
+
*
|
|
331
|
+
* @param {string} planningDir - Path to .planning directory
|
|
332
|
+
* @returns {{ action: string, message: string } | { disabled: true, message: string }}
|
|
333
|
+
*/
|
|
334
|
+
function intelUpdate(planningDir) {
|
|
335
|
+
if (!isIntelEnabled(planningDir)) return disabledResponse();
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
action: 'spawn_agent',
|
|
339
|
+
message: 'Run gsd-tools intel update or spawn gsd-intel-updater agent for full refresh'
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Save a refresh snapshot with hashes of all current intel files.
|
|
345
|
+
* Called by the intel-updater agent after completing a refresh.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} planningDir - Path to .planning directory
|
|
348
|
+
* @returns {{ saved: boolean, timestamp: string, files: number }}
|
|
349
|
+
*/
|
|
350
|
+
function saveRefreshSnapshot(planningDir) {
|
|
351
|
+
const intelPath = ensureIntelDir(planningDir);
|
|
352
|
+
const hashes = {};
|
|
353
|
+
let fileCount = 0;
|
|
354
|
+
|
|
355
|
+
for (const [_key, filename] of Object.entries(INTEL_FILES)) {
|
|
356
|
+
const filePath = path.join(intelPath, filename);
|
|
357
|
+
const hash = hashFile(filePath);
|
|
358
|
+
if (hash) {
|
|
359
|
+
hashes[filename] = hash;
|
|
360
|
+
fileCount++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const timestamp = new Date().toISOString();
|
|
365
|
+
const snapshotPath = path.join(intelPath, '.last-refresh.json');
|
|
366
|
+
fs.writeFileSync(snapshotPath, JSON.stringify({
|
|
367
|
+
hashes,
|
|
368
|
+
timestamp,
|
|
369
|
+
version: 1
|
|
370
|
+
}, null, 2), 'utf8');
|
|
371
|
+
|
|
372
|
+
return { saved: true, timestamp, files: fileCount };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ─── CLI Subcommands ─────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Thin wrapper around saveRefreshSnapshot for CLI dispatch.
|
|
379
|
+
* Writes .last-refresh.json with accurate timestamps and hashes.
|
|
380
|
+
*
|
|
381
|
+
* @param {string} planningDir - Path to .planning directory
|
|
382
|
+
* @returns {{ saved: boolean, timestamp: string, files: number } | { disabled: true, message: string }}
|
|
383
|
+
*/
|
|
384
|
+
function intelSnapshot(planningDir) {
|
|
385
|
+
if (!isIntelEnabled(planningDir)) return disabledResponse();
|
|
386
|
+
return saveRefreshSnapshot(planningDir);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Validate all intel files for correctness and freshness.
|
|
391
|
+
*
|
|
392
|
+
* @param {string} planningDir - Path to .planning directory
|
|
393
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] } | { disabled: true, message: string }}
|
|
394
|
+
*/
|
|
395
|
+
function intelValidate(planningDir) {
|
|
396
|
+
if (!isIntelEnabled(planningDir)) return disabledResponse();
|
|
397
|
+
|
|
398
|
+
const errors = [];
|
|
399
|
+
const warnings = [];
|
|
400
|
+
const STALE_MS = 24 * 60 * 60 * 1000;
|
|
401
|
+
const now = Date.now();
|
|
402
|
+
|
|
403
|
+
for (const [key, filename] of Object.entries(INTEL_FILES)) {
|
|
404
|
+
const filePath = intelFilePath(planningDir, filename);
|
|
405
|
+
|
|
406
|
+
// Check existence
|
|
407
|
+
if (!fs.existsSync(filePath)) {
|
|
408
|
+
errors.push(`${filename}: file does not exist`);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Skip non-JSON files (arch.md)
|
|
413
|
+
if (filename.endsWith('.md')) continue;
|
|
414
|
+
|
|
415
|
+
// Parse JSON
|
|
416
|
+
let data;
|
|
417
|
+
try {
|
|
418
|
+
data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
419
|
+
} catch (e) {
|
|
420
|
+
errors.push(`${filename}: invalid JSON — ${e.message}`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check _meta.updated_at recency
|
|
425
|
+
if (data._meta && data._meta.updated_at) {
|
|
426
|
+
const age = now - new Date(data._meta.updated_at).getTime();
|
|
427
|
+
if (age > STALE_MS) {
|
|
428
|
+
warnings.push(`${filename}: _meta.updated_at is ${Math.round(age / 3600000)} hours old (>24 hr)`);
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
warnings.push(`${filename}: missing _meta.updated_at`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Validate entries are objects with expected fields
|
|
435
|
+
if (data.entries && typeof data.entries === 'object') {
|
|
436
|
+
// files.json: check exports are actual symbol names (no spaces)
|
|
437
|
+
if (key === 'files') {
|
|
438
|
+
for (const [entryPath, entry] of Object.entries(data.entries)) {
|
|
439
|
+
if (entry.exports && Array.isArray(entry.exports)) {
|
|
440
|
+
for (const exp of entry.exports) {
|
|
441
|
+
if (typeof exp === 'string' && exp.includes(' ')) {
|
|
442
|
+
warnings.push(`${filename}: "${entryPath}" export "${exp}" looks like a description (contains space)`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Spot-check first 5 file paths exist on disk
|
|
448
|
+
const entryPaths = Object.keys(data.entries).slice(0, 5);
|
|
449
|
+
for (const ep of entryPaths) {
|
|
450
|
+
if (!fs.existsSync(ep)) {
|
|
451
|
+
warnings.push(`${filename}: entry path "${ep}" does not exist on disk`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// deps.json: check entries have version, type, used_by
|
|
457
|
+
if (key === 'deps') {
|
|
458
|
+
for (const [depName, entry] of Object.entries(data.entries)) {
|
|
459
|
+
const missing = [];
|
|
460
|
+
if (!entry.version) missing.push('version');
|
|
461
|
+
if (!entry.type) missing.push('type');
|
|
462
|
+
if (!entry.used_by) missing.push('used_by');
|
|
463
|
+
if (missing.length > 0) {
|
|
464
|
+
warnings.push(`${filename}: "${depName}" missing fields: ${missing.join(', ')}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* patch _meta.updated_at in a JSON intel file to the current timestamp.
|
|
476
|
+
* Reads the file, updates _meta.updated_at, increments version, writes back.
|
|
477
|
+
*
|
|
478
|
+
* NOTE: Does not gate on isIntelEnabled — operates on arbitrary file paths
|
|
479
|
+
* for use by agents patching individual files outside the intel store.
|
|
480
|
+
*
|
|
481
|
+
* @param {string} filePath - Absolute or relative path to the JSON intel file
|
|
482
|
+
* @returns {{ patched: boolean, file: string, timestamp: string } | { patched: false, error: string }}
|
|
483
|
+
*/
|
|
484
|
+
function intelPatchMeta(filePath) {
|
|
485
|
+
try {
|
|
486
|
+
if (!fs.existsSync(filePath)) {
|
|
487
|
+
return { patched: false, error: `File not found: ${filePath}` };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
491
|
+
let data;
|
|
492
|
+
try {
|
|
493
|
+
data = JSON.parse(content);
|
|
494
|
+
} catch (e) {
|
|
495
|
+
return { patched: false, error: `Invalid JSON: ${e.message}` };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!data._meta) {
|
|
499
|
+
data._meta = {};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const timestamp = new Date().toISOString();
|
|
503
|
+
data._meta.updated_at = timestamp;
|
|
504
|
+
data._meta.version = (data._meta.version || 0) + 1;
|
|
505
|
+
|
|
506
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
507
|
+
|
|
508
|
+
return { patched: true, file: filePath, timestamp };
|
|
509
|
+
} catch (e) {
|
|
510
|
+
return { patched: false, error: e.message };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Extract exports from a JS/CJS file by parsing module.exports or exports.X patterns.
|
|
516
|
+
*
|
|
517
|
+
* NOTE: Does not gate on isIntelEnabled — operates on arbitrary source files
|
|
518
|
+
* for use by agents building intel data from project files.
|
|
519
|
+
*
|
|
520
|
+
* @param {string} filePath - Path to the JS/CJS file
|
|
521
|
+
* @returns {{ file: string, exports: string[], method: string }}
|
|
522
|
+
*/
|
|
523
|
+
function intelExtractExports(filePath) {
|
|
524
|
+
if (!fs.existsSync(filePath)) {
|
|
525
|
+
return { file: filePath, exports: [], method: 'none' };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
529
|
+
let exports = [];
|
|
530
|
+
let method = 'none';
|
|
531
|
+
|
|
532
|
+
// Try module.exports = { ... } pattern (handle multi-line)
|
|
533
|
+
// Find the LAST module.exports assignment (the actual one, not references in code)
|
|
534
|
+
const allMatches = [...content.matchAll(/module\.exports\s*=\s*\{/g)];
|
|
535
|
+
if (allMatches.length > 0) {
|
|
536
|
+
const lastMatch = allMatches[allMatches.length - 1];
|
|
537
|
+
const startIdx = lastMatch.index + lastMatch[0].length;
|
|
538
|
+
// Find matching closing brace by counting braces
|
|
539
|
+
let depth = 1;
|
|
540
|
+
let endIdx = startIdx;
|
|
541
|
+
while (endIdx < content.length && depth > 0) {
|
|
542
|
+
if (content[endIdx] === '{') depth++;
|
|
543
|
+
else if (content[endIdx] === '}') depth--;
|
|
544
|
+
if (depth > 0) endIdx++;
|
|
545
|
+
}
|
|
546
|
+
const block = content.substring(startIdx, endIdx);
|
|
547
|
+
method = 'module.exports';
|
|
548
|
+
// Extract key names from lines like " keyName," or " keyName: value,"
|
|
549
|
+
const lines = block.split('\n');
|
|
550
|
+
for (const line of lines) {
|
|
551
|
+
const trimmed = line.trim();
|
|
552
|
+
// Skip comments and empty lines
|
|
553
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
|
|
554
|
+
// Match identifier at start of line (before comma, colon, end of line)
|
|
555
|
+
const keyMatch = trimmed.match(/^(\w+)\s*[,}:]/) || trimmed.match(/^(\w+)$/);
|
|
556
|
+
if (keyMatch) {
|
|
557
|
+
exports.push(keyMatch[1]);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Also try individual exports.X = patterns (only at start of line, not inside strings/regex)
|
|
563
|
+
const individualPattern = /^exports\.(\w+)\s*=/gm;
|
|
564
|
+
let im;
|
|
565
|
+
while ((im = individualPattern.exec(content)) !== null) {
|
|
566
|
+
if (!exports.includes(im[1])) {
|
|
567
|
+
exports.push(im[1]);
|
|
568
|
+
if (method === 'none') method = 'exports.X';
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const hadCjs = exports.length > 0;
|
|
573
|
+
|
|
574
|
+
// ESM patterns
|
|
575
|
+
const esmExports = [];
|
|
576
|
+
|
|
577
|
+
// export default function X / export default class X
|
|
578
|
+
const defaultNamedPattern = /^export\s+default\s+(?:function|class)\s+(\w+)/gm;
|
|
579
|
+
let em;
|
|
580
|
+
while ((em = defaultNamedPattern.exec(content)) !== null) {
|
|
581
|
+
if (!esmExports.includes(em[1])) esmExports.push(em[1]);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// export default (without named function/class)
|
|
585
|
+
const defaultAnonPattern = /^export\s+default\s+(?!function\s|class\s)/gm;
|
|
586
|
+
if (defaultAnonPattern.test(content) && esmExports.length === 0) {
|
|
587
|
+
if (!esmExports.includes('default')) esmExports.push('default');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// export function X( / export async function X(
|
|
591
|
+
const exportFnPattern = /^export\s+(?:async\s+)?function\s+(\w+)\s*\(/gm;
|
|
592
|
+
while ((em = exportFnPattern.exec(content)) !== null) {
|
|
593
|
+
if (!esmExports.includes(em[1])) esmExports.push(em[1]);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// export const X = / export let X = / export var X =
|
|
597
|
+
const exportVarPattern = /^export\s+(?:const|let|var)\s+(\w+)\s*=/gm;
|
|
598
|
+
while ((em = exportVarPattern.exec(content)) !== null) {
|
|
599
|
+
if (!esmExports.includes(em[1])) esmExports.push(em[1]);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// export class X
|
|
603
|
+
const exportClassPattern = /^export\s+class\s+(\w+)/gm;
|
|
604
|
+
while ((em = exportClassPattern.exec(content)) !== null) {
|
|
605
|
+
if (!esmExports.includes(em[1])) esmExports.push(em[1]);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// export { X, Y, Z } — strip "as alias" parts
|
|
609
|
+
const exportBlockPattern = /^export\s*\{([^}]+)\}/gm;
|
|
610
|
+
while ((em = exportBlockPattern.exec(content)) !== null) {
|
|
611
|
+
const items = em[1].split(',');
|
|
612
|
+
for (const item of items) {
|
|
613
|
+
const trimmed = item.trim();
|
|
614
|
+
if (!trimmed) continue;
|
|
615
|
+
// "foo as bar" -> extract "foo"
|
|
616
|
+
const name = trimmed.split(/\s+as\s+/)[0].trim();
|
|
617
|
+
if (name && !esmExports.includes(name)) esmExports.push(name);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Merge ESM exports into the result
|
|
622
|
+
for (const e of esmExports) {
|
|
623
|
+
if (!exports.includes(e)) exports.push(e);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Determine method
|
|
627
|
+
const hadEsm = esmExports.length > 0;
|
|
628
|
+
if (hadCjs && hadEsm) {
|
|
629
|
+
method = 'mixed';
|
|
630
|
+
} else if (hadEsm && !hadCjs) {
|
|
631
|
+
method = 'esm';
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return { file: filePath, exports, method };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ─── Exports ─────────────────────────────────────────────────────────────────
|
|
638
|
+
|
|
639
|
+
module.exports = {
|
|
640
|
+
// Public API
|
|
641
|
+
intelQuery,
|
|
642
|
+
intelUpdate,
|
|
643
|
+
intelStatus,
|
|
644
|
+
intelDiff,
|
|
645
|
+
saveRefreshSnapshot,
|
|
646
|
+
|
|
647
|
+
// CLI subcommands
|
|
648
|
+
intelSnapshot,
|
|
649
|
+
intelValidate,
|
|
650
|
+
intelExtractExports,
|
|
651
|
+
intelPatchMeta,
|
|
652
|
+
|
|
653
|
+
// Utilities
|
|
654
|
+
ensureIntelDir,
|
|
655
|
+
isIntelEnabled,
|
|
656
|
+
|
|
657
|
+
// Constants
|
|
658
|
+
INTEL_FILES,
|
|
659
|
+
INTEL_DIR
|
|
660
|
+
};
|