gsd-opencode 1.33.3 → 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-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-code-review-fix.md +52 -0
- package/commands/gsd/gsd-code-review.md +55 -0
- 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-import.md +36 -0
- package/commands/gsd/gsd-intel.md +183 -0
- package/commands/gsd/gsd-next.md +2 -0
- package/commands/gsd/gsd-reapply-patches.md +58 -3
- package/commands/gsd/gsd-review.md +4 -2
- package/commands/gsd/gsd-scan.md +26 -0
- 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
|
@@ -70,6 +70,16 @@
|
|
|
70
70
|
* audit-uat Scan all phases for unresolved UAT/verification items
|
|
71
71
|
* uat render-checkpoint --file <path> Render the current UAT checkpoint block
|
|
72
72
|
*
|
|
73
|
+
* Intel:
|
|
74
|
+
* intel query <term> Query intel files for a term
|
|
75
|
+
* intel status Show intel file freshness
|
|
76
|
+
* intel update Trigger intel refresh (returns agent spawn hint)
|
|
77
|
+
* intel diff Show changed intel entries since last snapshot
|
|
78
|
+
* intel snapshot Save current intel state as diff baseline
|
|
79
|
+
* intel patch-meta <file> Update _meta.updated_at in an intel file
|
|
80
|
+
* intel validate Validate intel file structure
|
|
81
|
+
* intel extract-exports <file> Extract exported symbols from a source file
|
|
82
|
+
*
|
|
73
83
|
* Scaffolding:
|
|
74
84
|
* scaffold context --phase <N> Create CONTEXT.md template
|
|
75
85
|
* scaffold uat --phase <N> Create UAT.md template
|
|
@@ -137,6 +147,17 @@
|
|
|
137
147
|
*
|
|
138
148
|
* Documentation:
|
|
139
149
|
* docs-init Project context for docs-update workflow
|
|
150
|
+
*
|
|
151
|
+
* Learnings:
|
|
152
|
+
* learnings list List all global learnings (JSON)
|
|
153
|
+
* learnings query --tag <tag> Query learnings by tag
|
|
154
|
+
* learnings copy Copy from current project's LEARNINGS.md
|
|
155
|
+
* learnings prune --older-than <dur> Remove entries older than duration (e.g. 90d)
|
|
156
|
+
* learnings delete <id> Delete a learning by ID
|
|
157
|
+
*
|
|
158
|
+
* GSD-2 Migration:
|
|
159
|
+
* from-gsd2 [--path <dir>] [--force] [--dry-run]
|
|
160
|
+
* Import a GSD-2 (.gsd/) project back to GSD v1 (.planning/) format
|
|
140
161
|
*/
|
|
141
162
|
|
|
142
163
|
const fs = require('fs');
|
|
@@ -157,6 +178,7 @@ const profilePipeline = require('./lib/profile-pipeline.cjs');
|
|
|
157
178
|
const profileOutput = require('./lib/profile-output.cjs');
|
|
158
179
|
const workstream = require('./lib/workstream.cjs');
|
|
159
180
|
const docs = require('./lib/docs.cjs');
|
|
181
|
+
const learnings = require('./lib/learnings.cjs');
|
|
160
182
|
|
|
161
183
|
// ─── Arg parsing helpers ──────────────────────────────────────────────────────
|
|
162
184
|
|
|
@@ -276,12 +298,33 @@ async function main() {
|
|
|
276
298
|
args.splice(pickIdx, 2);
|
|
277
299
|
}
|
|
278
300
|
|
|
301
|
+
// --default <value>: for config-get, return this value instead of erroring
|
|
302
|
+
// when the key is absent. Allows workflows to express optional config reads
|
|
303
|
+
// without defensive `2>/dev/null || true` boilerplate (#1893).
|
|
304
|
+
const defaultIdx = args.indexOf('--default');
|
|
305
|
+
let defaultValue = undefined;
|
|
306
|
+
if (defaultIdx !== -1) {
|
|
307
|
+
defaultValue = args[defaultIdx + 1];
|
|
308
|
+
if (defaultValue === undefined) defaultValue = '';
|
|
309
|
+
args.splice(defaultIdx, 2);
|
|
310
|
+
}
|
|
311
|
+
|
|
279
312
|
const command = args[0];
|
|
280
313
|
|
|
281
314
|
if (!command) {
|
|
282
315
|
error('Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-new-project, init, workstream, docs-init');
|
|
283
316
|
}
|
|
284
317
|
|
|
318
|
+
// Reject flags that are never valid for any gsd-tools command. AI agents
|
|
319
|
+
// sometimes hallucinate --help or --version on tool invocations; silently
|
|
320
|
+
// ignoring them can cause destructive operations to proceed unchecked.
|
|
321
|
+
const NEVER_VALID_FLAGS = new Set(['-h', '--help', '-?', '--h', '--version', '-v', '--usage']);
|
|
322
|
+
for (const arg of args) {
|
|
323
|
+
if (NEVER_VALID_FLAGS.has(arg)) {
|
|
324
|
+
error(`Unknown flag: ${arg}\ngsd-tools does not accept help or version flags. Run "gsd-tools" with no arguments for usage.`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
285
328
|
// Multi-repo guard: resolve project root for commands that read/write .planning/.
|
|
286
329
|
// Skip for pure-utility commands that don't touch .planning/ to avoid unnecessary
|
|
287
330
|
// filesystem traversal on every invocation.
|
|
@@ -318,7 +361,7 @@ async function main() {
|
|
|
318
361
|
}
|
|
319
362
|
};
|
|
320
363
|
try {
|
|
321
|
-
await runCommand(command, args, cwd, raw);
|
|
364
|
+
await runCommand(command, args, cwd, raw, defaultValue);
|
|
322
365
|
cleanup();
|
|
323
366
|
} catch (e) {
|
|
324
367
|
fs.writeSync = origWriteSync;
|
|
@@ -327,7 +370,27 @@ async function main() {
|
|
|
327
370
|
return;
|
|
328
371
|
}
|
|
329
372
|
|
|
330
|
-
|
|
373
|
+
// Intercept stdout to transparently resolve @file: references (#1891).
|
|
374
|
+
// core.cjs output() writes @file:<path> when JSON > 50KB. The --pick path
|
|
375
|
+
// already resolves this, but the normal path wrote @file: to stdout, forcing
|
|
376
|
+
// every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
|
|
377
|
+
// that breaks on PowerShell and other non-bash shells.
|
|
378
|
+
const origWriteSync2 = fs.writeSync;
|
|
379
|
+
const outChunks = [];
|
|
380
|
+
fs.writeSync = function (fd, data, ...rest) {
|
|
381
|
+
if (fd === 1) { outChunks.push(String(data)); return; }
|
|
382
|
+
return origWriteSync2.call(fs, fd, data, ...rest);
|
|
383
|
+
};
|
|
384
|
+
try {
|
|
385
|
+
await runCommand(command, args, cwd, raw, defaultValue);
|
|
386
|
+
} finally {
|
|
387
|
+
fs.writeSync = origWriteSync2;
|
|
388
|
+
}
|
|
389
|
+
let captured = outChunks.join('');
|
|
390
|
+
if (captured.startsWith('@file:')) {
|
|
391
|
+
captured = fs.readFileSync(captured.slice(6), 'utf-8');
|
|
392
|
+
}
|
|
393
|
+
origWriteSync2.call(fs, 1, captured);
|
|
331
394
|
}
|
|
332
395
|
|
|
333
396
|
/**
|
|
@@ -353,7 +416,7 @@ function extractField(obj, fieldPath) {
|
|
|
353
416
|
return current;
|
|
354
417
|
}
|
|
355
418
|
|
|
356
|
-
async function runCommand(command, args, cwd, raw) {
|
|
419
|
+
async function runCommand(command, args, cwd, raw, defaultValue) {
|
|
357
420
|
switch (command) {
|
|
358
421
|
case 'state': {
|
|
359
422
|
const subcommand = args[1];
|
|
@@ -561,7 +624,7 @@ async function runCommand(command, args, cwd, raw) {
|
|
|
561
624
|
}
|
|
562
625
|
|
|
563
626
|
case 'config-get': {
|
|
564
|
-
config.cmdConfigGet(cwd, args[1], raw);
|
|
627
|
+
config.cmdConfigGet(cwd, args[1], raw, defaultValue);
|
|
565
628
|
break;
|
|
566
629
|
}
|
|
567
630
|
|
|
@@ -592,7 +655,7 @@ async function runCommand(command, args, cwd, raw) {
|
|
|
592
655
|
};
|
|
593
656
|
phase.cmdPhasesList(cwd, options, raw);
|
|
594
657
|
} else if (subcommand === 'clear') {
|
|
595
|
-
milestone.cmdPhasesClear(cwd, raw);
|
|
658
|
+
milestone.cmdPhasesClear(cwd, raw, args.slice(2));
|
|
596
659
|
} else {
|
|
597
660
|
error('Unknown phases subcommand. Available: list, clear');
|
|
598
661
|
}
|
|
@@ -937,6 +1000,45 @@ async function runCommand(command, args, cwd, raw) {
|
|
|
937
1000
|
break;
|
|
938
1001
|
}
|
|
939
1002
|
|
|
1003
|
+
// ─── Intel ────────────────────────────────────────────────────────────
|
|
1004
|
+
|
|
1005
|
+
case 'intel': {
|
|
1006
|
+
const intel = require('./lib/intel.cjs');
|
|
1007
|
+
const subcommand = args[1];
|
|
1008
|
+
if (subcommand === 'query') {
|
|
1009
|
+
const term = args[2];
|
|
1010
|
+
if (!term) error('Usage: gsd-tools intel query <term>');
|
|
1011
|
+
const planningDir = path.join(cwd, '.planning');
|
|
1012
|
+
core.output(intel.intelQuery(term, planningDir), raw);
|
|
1013
|
+
} else if (subcommand === 'status') {
|
|
1014
|
+
const planningDir = path.join(cwd, '.planning');
|
|
1015
|
+
core.output(intel.intelStatus(planningDir), raw);
|
|
1016
|
+
} else if (subcommand === 'diff') {
|
|
1017
|
+
const planningDir = path.join(cwd, '.planning');
|
|
1018
|
+
core.output(intel.intelDiff(planningDir), raw);
|
|
1019
|
+
} else if (subcommand === 'snapshot') {
|
|
1020
|
+
const planningDir = path.join(cwd, '.planning');
|
|
1021
|
+
core.output(intel.intelSnapshot(planningDir), raw);
|
|
1022
|
+
} else if (subcommand === 'patch-meta') {
|
|
1023
|
+
const filePath = args[2];
|
|
1024
|
+
if (!filePath) error('Usage: gsd-tools intel patch-meta <file-path>');
|
|
1025
|
+
core.output(intel.intelPatchMeta(path.resolve(cwd, filePath)), raw);
|
|
1026
|
+
} else if (subcommand === 'validate') {
|
|
1027
|
+
const planningDir = path.join(cwd, '.planning');
|
|
1028
|
+
core.output(intel.intelValidate(planningDir), raw);
|
|
1029
|
+
} else if (subcommand === 'extract-exports') {
|
|
1030
|
+
const filePath = args[2];
|
|
1031
|
+
if (!filePath) error('Usage: gsd-tools intel extract-exports <file-path>');
|
|
1032
|
+
core.output(intel.intelExtractExports(path.resolve(cwd, filePath)), raw);
|
|
1033
|
+
} else if (subcommand === 'update') {
|
|
1034
|
+
const planningDir = path.join(cwd, '.planning');
|
|
1035
|
+
core.output(intel.intelUpdate(planningDir), raw);
|
|
1036
|
+
} else {
|
|
1037
|
+
error('Unknown intel subcommand. Available: query, status, update, diff, snapshot, patch-meta, validate, extract-exports');
|
|
1038
|
+
}
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
940
1042
|
// ─── Documentation ────────────────────────────────────────────────────
|
|
941
1043
|
|
|
942
1044
|
case 'docs-init': {
|
|
@@ -944,6 +1046,42 @@ async function runCommand(command, args, cwd, raw) {
|
|
|
944
1046
|
break;
|
|
945
1047
|
}
|
|
946
1048
|
|
|
1049
|
+
// ─── Learnings ─────────────────────────────────────────────────────────
|
|
1050
|
+
|
|
1051
|
+
case 'learnings': {
|
|
1052
|
+
const subcommand = args[1];
|
|
1053
|
+
if (subcommand === 'list') {
|
|
1054
|
+
learnings.cmdLearningsList(raw);
|
|
1055
|
+
} else if (subcommand === 'query') {
|
|
1056
|
+
const tagIdx = args.indexOf('--tag');
|
|
1057
|
+
const tag = tagIdx !== -1 ? args[tagIdx + 1] : null;
|
|
1058
|
+
if (!tag) error('Usage: gsd-tools learnings query --tag <tag>');
|
|
1059
|
+
learnings.cmdLearningsQuery(tag, raw);
|
|
1060
|
+
} else if (subcommand === 'copy') {
|
|
1061
|
+
learnings.cmdLearningsCopy(cwd, raw);
|
|
1062
|
+
} else if (subcommand === 'prune') {
|
|
1063
|
+
const olderIdx = args.indexOf('--older-than');
|
|
1064
|
+
const olderThan = olderIdx !== -1 ? args[olderIdx + 1] : null;
|
|
1065
|
+
if (!olderThan) error('Usage: gsd-tools learnings prune --older-than <duration>');
|
|
1066
|
+
learnings.cmdLearningsPrune(olderThan, raw);
|
|
1067
|
+
} else if (subcommand === 'delete') {
|
|
1068
|
+
const id = args[2];
|
|
1069
|
+
if (!id) error('Usage: gsd-tools learnings delete <id>');
|
|
1070
|
+
learnings.cmdLearningsDelete(id, raw);
|
|
1071
|
+
} else {
|
|
1072
|
+
error('Unknown learnings subcommand. Available: list, query, copy, prune, delete');
|
|
1073
|
+
}
|
|
1074
|
+
break;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// ─── GSD-2 Reverse Migration ───────────────────────────────────────────
|
|
1078
|
+
|
|
1079
|
+
case 'from-gsd2': {
|
|
1080
|
+
const gsd2Import = require('./lib/gsd2-import.cjs');
|
|
1081
|
+
gsd2Import.cmdFromGsd2(args.slice(1), cwd, raw);
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
947
1085
|
default:
|
|
948
1086
|
error(`Unknown command: ${command}`);
|
|
949
1087
|
}
|
|
@@ -313,11 +313,19 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
// Stage files
|
|
316
|
-
const
|
|
316
|
+
const explicitFiles = files && files.length > 0;
|
|
317
|
+
const filesToStage = explicitFiles ? files : ['.planning/'];
|
|
317
318
|
for (const file of filesToStage) {
|
|
318
319
|
const fullPath = path.join(cwd, file);
|
|
319
320
|
if (!fs.existsSync(fullPath)) {
|
|
320
|
-
|
|
321
|
+
if (explicitFiles) {
|
|
322
|
+
// Caller passed an explicit --files list: missing files are skipped.
|
|
323
|
+
// Staging a deletion here would silently remove tracked planning files
|
|
324
|
+
// (e.g. STATE.md, ROADMAP.md) when they are temporarily absent (#2014).
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// Default mode (staging all of .planning/): stage the deletion so
|
|
328
|
+
// removed planning files are not left dangling in the index.
|
|
321
329
|
execGit(cwd, ['rm', '--cached', '--ignore-unmatch', file]);
|
|
322
330
|
} else {
|
|
323
331
|
execGit(cwd, ['add', file]);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { output, error,
|
|
7
|
+
const { output, error, planningDir, withPlanningLock, CONFIG_DEFAULTS, atomicWriteFileSync } = require('./core.cjs');
|
|
8
8
|
const {
|
|
9
9
|
VALID_PROFILES,
|
|
10
10
|
getAgentToModelMapForProfile,
|
|
@@ -15,7 +15,7 @@ const VALID_CONFIG_KEYS = new Set([
|
|
|
15
15
|
'mode', 'granularity', 'parallelization', 'commit_docs', 'model_profile',
|
|
16
16
|
'search_gitignored', 'brave_search', 'firecrawl', 'exa_search',
|
|
17
17
|
'workflow.research', 'workflow.plan_check', 'workflow.verifier',
|
|
18
|
-
'workflow.nyquist_validation', 'workflow.ui_phase', 'workflow.ui_safety_gate',
|
|
18
|
+
'workflow.nyquist_validation', 'workflow.ai_integration_phase', 'workflow.ui_phase', 'workflow.ui_safety_gate',
|
|
19
19
|
'workflow.auto_advance', 'workflow.node_repair', 'workflow.node_repair_budget',
|
|
20
20
|
'workflow.text_mode',
|
|
21
21
|
'workflow.research_before_questions',
|
|
@@ -23,13 +23,20 @@ const VALID_CONFIG_KEYS = new Set([
|
|
|
23
23
|
'workflow.skip_discuss',
|
|
24
24
|
'workflow._auto_chain_active',
|
|
25
25
|
'workflow.use_worktrees',
|
|
26
|
+
'workflow.code_review',
|
|
27
|
+
'workflow.code_review_depth',
|
|
26
28
|
'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
|
|
27
29
|
'planning.commit_docs', 'planning.search_gitignored',
|
|
28
30
|
'workflow.subagent_timeout',
|
|
29
31
|
'hooks.context_warnings',
|
|
32
|
+
'features.thinking_partner',
|
|
33
|
+
'context',
|
|
34
|
+
'features.global_learnings',
|
|
35
|
+
'learnings.max_inject',
|
|
30
36
|
'project_code', 'phase_naming',
|
|
31
37
|
'manager.flags.discuss', 'manager.flags.plan', 'manager.flags.execute',
|
|
32
38
|
'response_language',
|
|
39
|
+
'intel.enabled',
|
|
33
40
|
]);
|
|
34
41
|
|
|
35
42
|
/**
|
|
@@ -41,6 +48,12 @@ function isValidConfigKey(keyPath) {
|
|
|
41
48
|
if (VALID_CONFIG_KEYS.has(keyPath)) return true;
|
|
42
49
|
// Allow agent_skills.<agent-type> with any agent type string
|
|
43
50
|
if (/^agent_skills\.[a-zA-Z0-9_-]+$/.test(keyPath)) return true;
|
|
51
|
+
// Allow review.models.<cli-name> for per-CLI model selection in /gsd-review
|
|
52
|
+
if (/^review\.models\.[a-zA-Z0-9_-]+$/.test(keyPath)) return true;
|
|
53
|
+
// Allow features.<feature_name> — dynamic namespace for feature flags.
|
|
54
|
+
// Intentionally open-ended so new flags (e.g., features.global_learnings) work
|
|
55
|
+
// without updating VALID_CONFIG_KEYS each time.
|
|
56
|
+
if (/^features\.[a-zA-Z0-9_]+$/.test(keyPath)) return true;
|
|
44
57
|
return false;
|
|
45
58
|
}
|
|
46
59
|
|
|
@@ -50,6 +63,11 @@ const CONFIG_KEY_SUGGESTIONS = {
|
|
|
50
63
|
'nyquist.validation_enabled': 'workflow.nyquist_validation',
|
|
51
64
|
'hooks.research_questions': 'workflow.research_before_questions',
|
|
52
65
|
'workflow.research_questions': 'workflow.research_before_questions',
|
|
66
|
+
'workflow.codereview': 'workflow.code_review',
|
|
67
|
+
'workflow.review': 'workflow.code_review',
|
|
68
|
+
'workflow.code_review_level': 'workflow.code_review_depth',
|
|
69
|
+
'workflow.review_depth': 'workflow.code_review_depth',
|
|
70
|
+
'review.model': 'review.models.<cli-name>',
|
|
53
71
|
};
|
|
54
72
|
|
|
55
73
|
function validateKnownConfigKeyPath(keyPath) {
|
|
@@ -129,10 +147,13 @@ function buildNewProjectConfig(userChoices) {
|
|
|
129
147
|
node_repair_budget: 2,
|
|
130
148
|
ui_phase: true,
|
|
131
149
|
ui_safety_gate: true,
|
|
150
|
+
ai_integration_phase: true,
|
|
132
151
|
text_mode: false,
|
|
133
152
|
research_before_questions: false,
|
|
134
153
|
discuss_mode: 'discuss',
|
|
135
154
|
skip_discuss: false,
|
|
155
|
+
code_review: true,
|
|
156
|
+
code_review_depth: 'standard',
|
|
136
157
|
},
|
|
137
158
|
hooks: {
|
|
138
159
|
context_warnings: true,
|
|
@@ -180,7 +201,7 @@ function buildNewProjectConfig(userChoices) {
|
|
|
180
201
|
* Idempotent: if config.json already exists, returns { created: false }.
|
|
181
202
|
*/
|
|
182
203
|
function cmdConfigNewProject(cwd, choicesJson, raw) {
|
|
183
|
-
const planningBase =
|
|
204
|
+
const planningBase = planningDir(cwd);
|
|
184
205
|
const configPath = path.join(planningBase, 'config.json');
|
|
185
206
|
|
|
186
207
|
// Idempotent: don't overwrite existing config
|
|
@@ -211,7 +232,7 @@ function cmdConfigNewProject(cwd, choicesJson, raw) {
|
|
|
211
232
|
const config = buildNewProjectConfig(userChoices);
|
|
212
233
|
|
|
213
234
|
try {
|
|
214
|
-
|
|
235
|
+
atomicWriteFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
215
236
|
output({ created: true, path: '.planning/config.json' }, raw, 'created');
|
|
216
237
|
} catch (err) {
|
|
217
238
|
error('Failed to write config.json: ' + err.message);
|
|
@@ -225,7 +246,7 @@ function cmdConfigNewProject(cwd, choicesJson, raw) {
|
|
|
225
246
|
* the happy path. But note that `error()` will still `exit(1)` out of the process.
|
|
226
247
|
*/
|
|
227
248
|
function ensureConfigFile(cwd) {
|
|
228
|
-
const planningBase =
|
|
249
|
+
const planningBase = planningDir(cwd);
|
|
229
250
|
const configPath = path.join(planningBase, 'config.json');
|
|
230
251
|
|
|
231
252
|
// Ensure .planning directory exists
|
|
@@ -245,7 +266,7 @@ function ensureConfigFile(cwd) {
|
|
|
245
266
|
const config = buildNewProjectConfig({});
|
|
246
267
|
|
|
247
268
|
try {
|
|
248
|
-
|
|
269
|
+
atomicWriteFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
249
270
|
return { created: true, path: '.planning/config.json' };
|
|
250
271
|
} catch (err) {
|
|
251
272
|
error('Failed to create config.json: ' + err.message);
|
|
@@ -275,38 +296,40 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
275
296
|
* the happy path. But note that `error()` will still `exit(1)` out of the process.
|
|
276
297
|
*/
|
|
277
298
|
function setConfigValue(cwd, keyPath, parsedValue) {
|
|
278
|
-
const configPath = path.join(
|
|
299
|
+
const configPath = path.join(planningDir(cwd), 'config.json');
|
|
279
300
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
301
|
+
return withPlanningLock(cwd, () => {
|
|
302
|
+
// Load existing config or start with empty object
|
|
303
|
+
let config = {};
|
|
304
|
+
try {
|
|
305
|
+
if (fs.existsSync(configPath)) {
|
|
306
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
307
|
+
}
|
|
308
|
+
} catch (err) {
|
|
309
|
+
error('Failed to read config.json: ' + err.message);
|
|
285
310
|
}
|
|
286
|
-
} catch (err) {
|
|
287
|
-
error('Failed to read config.json: ' + err.message);
|
|
288
|
-
}
|
|
289
311
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
312
|
+
// Set nested value using dot notation (e.g., "workflow.research")
|
|
313
|
+
const keys = keyPath.split('.');
|
|
314
|
+
let current = config;
|
|
315
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
316
|
+
const key = keys[i];
|
|
317
|
+
if (current[key] === undefined || typeof current[key] !== 'object') {
|
|
318
|
+
current[key] = {};
|
|
319
|
+
}
|
|
320
|
+
current = current[key];
|
|
297
321
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const previousValue = current[keys[keys.length - 1]]; // Capture previous value before overwriting
|
|
301
|
-
current[keys[keys.length - 1]] = parsedValue;
|
|
322
|
+
const previousValue = current[keys[keys.length - 1]]; // Capture previous value before overwriting
|
|
323
|
+
current[keys[keys.length - 1]] = parsedValue;
|
|
302
324
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
325
|
+
// write back
|
|
326
|
+
try {
|
|
327
|
+
atomicWriteFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
328
|
+
return { updated: true, key: keyPath, value: parsedValue, previousValue };
|
|
329
|
+
} catch (err) {
|
|
330
|
+
error('Failed to write config.json: ' + err.message);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
310
333
|
}
|
|
311
334
|
|
|
312
335
|
/**
|
|
@@ -324,7 +347,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
324
347
|
validateKnownConfigKeyPath(keyPath);
|
|
325
348
|
|
|
326
349
|
if (!isValidConfigKey(keyPath)) {
|
|
327
|
-
error(`Unknown config key: "${keyPath}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>`);
|
|
350
|
+
error(`Unknown config key: "${keyPath}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>, features.<feature_name>`);
|
|
328
351
|
}
|
|
329
352
|
|
|
330
353
|
// Parse value (handle booleans, numbers, and JSON arrays/objects)
|
|
@@ -336,21 +359,30 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
336
359
|
try { parsedValue = JSON.parse(value); } catch { /* keep as string */ }
|
|
337
360
|
}
|
|
338
361
|
|
|
362
|
+
const VALID_CONTEXT_VALUES = ['dev', 'research', 'review'];
|
|
363
|
+
if (keyPath === 'context' && !VALID_CONTEXT_VALUES.includes(String(parsedValue))) {
|
|
364
|
+
error(`Invalid context value '${value}'. Valid values: ${VALID_CONTEXT_VALUES.join(', ')}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
339
367
|
const setConfigValueResult = setConfigValue(cwd, keyPath, parsedValue);
|
|
340
368
|
output(setConfigValueResult, raw, `${keyPath}=${parsedValue}`);
|
|
341
369
|
}
|
|
342
370
|
|
|
343
|
-
function cmdConfigGet(cwd, keyPath, raw) {
|
|
344
|
-
const configPath = path.join(
|
|
371
|
+
function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
372
|
+
const configPath = path.join(planningDir(cwd), 'config.json');
|
|
373
|
+
const hasDefault = defaultValue !== undefined;
|
|
345
374
|
|
|
346
375
|
if (!keyPath) {
|
|
347
|
-
error('Usage: config-get <key.path>');
|
|
376
|
+
error('Usage: config-get <key.path> [--default <value>]');
|
|
348
377
|
}
|
|
349
378
|
|
|
350
379
|
let config = {};
|
|
351
380
|
try {
|
|
352
381
|
if (fs.existsSync(configPath)) {
|
|
353
382
|
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
383
|
+
} else if (hasDefault) {
|
|
384
|
+
output(defaultValue, raw, String(defaultValue));
|
|
385
|
+
return;
|
|
354
386
|
} else {
|
|
355
387
|
error('No config.json found at ' + configPath);
|
|
356
388
|
}
|
|
@@ -364,12 +396,14 @@ function cmdConfigGet(cwd, keyPath, raw) {
|
|
|
364
396
|
let current = config;
|
|
365
397
|
for (const key of keys) {
|
|
366
398
|
if (current === undefined || current === null || typeof current !== 'object') {
|
|
399
|
+
if (hasDefault) { output(defaultValue, raw, String(defaultValue)); return; }
|
|
367
400
|
error(`Key not found: ${keyPath}`);
|
|
368
401
|
}
|
|
369
402
|
current = current[key];
|
|
370
403
|
}
|
|
371
404
|
|
|
372
405
|
if (current === undefined) {
|
|
406
|
+
if (hasDefault) { output(defaultValue, raw, String(defaultValue)); return; }
|
|
373
407
|
error(`Key not found: ${keyPath}`);
|
|
374
408
|
}
|
|
375
409
|
|
|
@@ -27,6 +27,16 @@ const WORKSTREAM_SESSION_ENV_KEYS = [
|
|
|
27
27
|
let cachedControllingTtyToken = null;
|
|
28
28
|
let didProbeControllingTtyToken = false;
|
|
29
29
|
|
|
30
|
+
// Track all .planning/.lock files held by this process so they can be removed
|
|
31
|
+
// on exit. process.on('exit') fires even on process.exit(1), unlike try/finally
|
|
32
|
+
// which is skipped when error() calls process.exit(1) inside a locked region (#1916).
|
|
33
|
+
const _heldPlanningLocks = new Set();
|
|
34
|
+
process.on('exit', () => {
|
|
35
|
+
for (const lockPath of _heldPlanningLocks) {
|
|
36
|
+
try { fs.unlinkSync(lockPath); } catch { /* already gone */ }
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
30
40
|
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
31
41
|
|
|
32
42
|
/** Normalize a relative path to always use forward slashes (cross-platform). */
|
|
@@ -229,6 +239,7 @@ const CONFIG_DEFAULTS = {
|
|
|
229
239
|
plan_checker: true,
|
|
230
240
|
verifier: true,
|
|
231
241
|
nyquist_validation: true,
|
|
242
|
+
ai_integration_phase: true,
|
|
232
243
|
parallelization: true,
|
|
233
244
|
brave_search: false,
|
|
234
245
|
firecrawl: false,
|
|
@@ -300,7 +311,7 @@ function loadConfig(cwd) {
|
|
|
300
311
|
// Extract top-level key names from dot-notation paths (e.g., 'workflow.research' → 'workflow')
|
|
301
312
|
...[...VALID_CONFIG_KEYS].map(k => k.split('.')[0]),
|
|
302
313
|
// Section containers that hold nested sub-keys
|
|
303
|
-
'git', 'workflow', 'planning', 'hooks',
|
|
314
|
+
'git', 'workflow', 'planning', 'hooks', 'features',
|
|
304
315
|
// Internal keys loadConfig reads but config-set doesn't expose
|
|
305
316
|
'model_overrides', 'agent_skills', 'context_window', 'resolve_model_ids',
|
|
306
317
|
// Deprecated keys (still accepted for migration, not in config-set)
|
|
@@ -400,7 +411,11 @@ function loadConfig(cwd) {
|
|
|
400
411
|
|
|
401
412
|
// ─── Git utilities ────────────────────────────────────────────────────────────
|
|
402
413
|
|
|
414
|
+
const _gitIgnoredCache = new Map();
|
|
415
|
+
|
|
403
416
|
function isGitIgnored(cwd, targetPath) {
|
|
417
|
+
const key = cwd + '::' + targetPath;
|
|
418
|
+
if (_gitIgnoredCache.has(key)) return _gitIgnoredCache.get(key);
|
|
404
419
|
try {
|
|
405
420
|
// --no-index checks .gitignore rules regardless of whether the file is tracked.
|
|
406
421
|
// Without it, git check-ignore returns "not ignored" for tracked files even when
|
|
@@ -412,8 +427,10 @@ function isGitIgnored(cwd, targetPath) {
|
|
|
412
427
|
cwd,
|
|
413
428
|
stdio: 'pipe',
|
|
414
429
|
});
|
|
430
|
+
_gitIgnoredCache.set(key, true);
|
|
415
431
|
return true;
|
|
416
432
|
} catch {
|
|
433
|
+
_gitIgnoredCache.set(key, false);
|
|
417
434
|
return false;
|
|
418
435
|
}
|
|
419
436
|
}
|
|
@@ -598,10 +615,15 @@ function withPlanningLock(cwd, fn) {
|
|
|
598
615
|
acquired: new Date().toISOString(),
|
|
599
616
|
}), { flag: 'wx' });
|
|
600
617
|
|
|
618
|
+
// Register for exit-time cleanup so process.exit(1) inside a locked region
|
|
619
|
+
// cannot leave a stale lock file (#1916).
|
|
620
|
+
_heldPlanningLocks.add(lockPath);
|
|
621
|
+
|
|
601
622
|
// Lock acquired — run the function
|
|
602
623
|
try {
|
|
603
624
|
return fn();
|
|
604
625
|
} finally {
|
|
626
|
+
_heldPlanningLocks.delete(lockPath);
|
|
605
627
|
try { fs.unlinkSync(lockPath); } catch { /* already released */ }
|
|
606
628
|
}
|
|
607
629
|
} catch (err) {
|
|
@@ -670,19 +692,23 @@ function planningRoot(cwd) {
|
|
|
670
692
|
}
|
|
671
693
|
|
|
672
694
|
/**
|
|
673
|
-
* Get common .planning file paths, workstream-aware.
|
|
674
|
-
*
|
|
675
|
-
*
|
|
695
|
+
* Get common .planning file paths, project-and-workstream-aware.
|
|
696
|
+
*
|
|
697
|
+
* All paths route through planningDir(cwd, ws), which honors the GSD_PROJECT
|
|
698
|
+
* env var and active workstream. This matches loadConfig() above (line 256),
|
|
699
|
+
* which has always read config.json via planningDir(cwd). Previously project
|
|
700
|
+
* and config were resolved against the unrouted .planning/ root, which broke
|
|
701
|
+
* `gsd-tools config-get` in multi-project layouts (the CRUD writers and the
|
|
702
|
+
* reader pointed at different files).
|
|
676
703
|
*/
|
|
677
704
|
function planningPaths(cwd, ws) {
|
|
678
705
|
const base = planningDir(cwd, ws);
|
|
679
|
-
const root = path.join(cwd, '.planning');
|
|
680
706
|
return {
|
|
681
707
|
planning: base,
|
|
682
708
|
state: path.join(base, 'STATE.md'),
|
|
683
709
|
roadmap: path.join(base, 'ROADMAP.md'),
|
|
684
|
-
project: path.join(
|
|
685
|
-
config: path.join(
|
|
710
|
+
project: path.join(base, 'PROJECT.md'),
|
|
711
|
+
config: path.join(base, 'config.json'),
|
|
686
712
|
phases: path.join(base, 'phases'),
|
|
687
713
|
requirements: path.join(base, 'REQUIREMENTS.md'),
|
|
688
714
|
};
|
|
@@ -879,7 +905,10 @@ function normalizePhaseName(phase) {
|
|
|
879
905
|
const match = stripped.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
880
906
|
if (match) {
|
|
881
907
|
const padded = match[1].padStart(2, '0');
|
|
882
|
-
|
|
908
|
+
// Preserve original case of letter suffix (#1962).
|
|
909
|
+
// Uppercasing causes directory/roadmap mismatches on case-sensitive filesystems
|
|
910
|
+
// (e.g., "16c" in ROADMAP.md → directory "16C-name" → progress can't match).
|
|
911
|
+
const letter = match[2] || '';
|
|
883
912
|
const decimal = match[3] || '';
|
|
884
913
|
return padded + letter + decimal;
|
|
885
914
|
}
|
|
@@ -1485,6 +1514,38 @@ function readSubdirectories(dirPath, sort = false) {
|
|
|
1485
1514
|
}
|
|
1486
1515
|
}
|
|
1487
1516
|
|
|
1517
|
+
// ─── Atomic file writes ───────────────────────────────────────────────────────
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* write a file atomically using write-to-temp-then-rename.
|
|
1521
|
+
*
|
|
1522
|
+
* On POSIX systems, `fs.renameSync` is atomic when the source and destination
|
|
1523
|
+
* are on the same filesystem. This prevents a process killed mid-write from
|
|
1524
|
+
* leaving a truncated file that is unparseable on next read.
|
|
1525
|
+
*
|
|
1526
|
+
* The temp file is placed alongside the target so it is guaranteed to be on
|
|
1527
|
+
* the same filesystem (required for rename atomicity). The PID is embedded in
|
|
1528
|
+
* the temp file name so concurrent writers use distinct paths.
|
|
1529
|
+
*
|
|
1530
|
+
* If `renameSync` fails (e.g. cross-device move), the function falls back to a
|
|
1531
|
+
* direct `writeFileSync` so callers always get a best-effort write.
|
|
1532
|
+
*
|
|
1533
|
+
* @param {string} filePath Absolute path to write.
|
|
1534
|
+
* @param {string|Buffer} content File content.
|
|
1535
|
+
* @param {string} [encoding='utf-8'] Encoding passed to writeFileSync.
|
|
1536
|
+
*/
|
|
1537
|
+
function atomicWriteFileSync(filePath, content, encoding = 'utf-8') {
|
|
1538
|
+
const tmpPath = filePath + '.tmp.' + process.pid;
|
|
1539
|
+
try {
|
|
1540
|
+
fs.writeFileSync(tmpPath, content, encoding);
|
|
1541
|
+
fs.renameSync(tmpPath, filePath);
|
|
1542
|
+
} catch (renameErr) {
|
|
1543
|
+
// Clean up the temp file if rename failed, then fall back to direct write.
|
|
1544
|
+
try { fs.unlinkSync(tmpPath); } catch { /* already gone or never created */ }
|
|
1545
|
+
fs.writeFileSync(filePath, content, encoding);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1488
1549
|
module.exports = {
|
|
1489
1550
|
output,
|
|
1490
1551
|
error,
|
|
@@ -1530,4 +1591,5 @@ module.exports = {
|
|
|
1530
1591
|
readSubdirectories,
|
|
1531
1592
|
getAgentsDir,
|
|
1532
1593
|
checkAgentsInstalled,
|
|
1594
|
+
atomicWriteFileSync,
|
|
1533
1595
|
};
|