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.
Files changed (130) hide show
  1. package/agents/gsd-advisor-researcher.md +23 -0
  2. package/agents/gsd-ai-researcher.md +142 -0
  3. package/agents/gsd-code-fixer.md +523 -0
  4. package/agents/gsd-code-reviewer.md +361 -0
  5. package/agents/gsd-debugger.md +14 -1
  6. package/agents/gsd-domain-researcher.md +162 -0
  7. package/agents/gsd-eval-auditor.md +170 -0
  8. package/agents/gsd-eval-planner.md +161 -0
  9. package/agents/gsd-executor.md +70 -7
  10. package/agents/gsd-framework-selector.md +167 -0
  11. package/agents/gsd-intel-updater.md +320 -0
  12. package/agents/gsd-phase-researcher.md +26 -0
  13. package/agents/gsd-plan-checker.md +12 -0
  14. package/agents/gsd-planner.md +16 -6
  15. package/agents/gsd-project-researcher.md +23 -0
  16. package/agents/gsd-ui-researcher.md +23 -0
  17. package/agents/gsd-verifier.md +55 -1
  18. package/commands/gsd/gsd-add-backlog.md +1 -1
  19. package/commands/gsd/gsd-add-phase.md +1 -1
  20. package/commands/gsd/gsd-add-todo.md +1 -1
  21. package/commands/gsd/gsd-ai-integration-phase.md +36 -0
  22. package/commands/gsd/gsd-audit-fix.md +33 -0
  23. package/commands/gsd/gsd-autonomous.md +1 -0
  24. package/commands/gsd/gsd-check-todos.md +1 -1
  25. package/commands/gsd/gsd-code-review-fix.md +52 -0
  26. package/commands/gsd/gsd-code-review.md +55 -0
  27. package/commands/gsd/gsd-complete-milestone.md +1 -1
  28. package/commands/gsd/gsd-debug.md +1 -1
  29. package/commands/gsd/gsd-eval-review.md +32 -0
  30. package/commands/gsd/gsd-explore.md +27 -0
  31. package/commands/gsd/gsd-from-gsd2.md +45 -0
  32. package/commands/gsd/gsd-health.md +1 -1
  33. package/commands/gsd/gsd-import.md +36 -0
  34. package/commands/gsd/gsd-insert-phase.md +1 -1
  35. package/commands/gsd/gsd-intel.md +183 -0
  36. package/commands/gsd/gsd-manager.md +1 -1
  37. package/commands/gsd/gsd-next.md +2 -0
  38. package/commands/gsd/gsd-reapply-patches.md +58 -3
  39. package/commands/gsd/gsd-remove-phase.md +1 -1
  40. package/commands/gsd/gsd-review.md +4 -2
  41. package/commands/gsd/gsd-scan.md +26 -0
  42. package/commands/gsd/gsd-set-profile.md +1 -1
  43. package/commands/gsd/gsd-thread.md +1 -1
  44. package/commands/gsd/gsd-undo.md +34 -0
  45. package/commands/gsd/gsd-workstreams.md +6 -6
  46. package/get-shit-done/bin/gsd-tools.cjs +143 -5
  47. package/get-shit-done/bin/lib/commands.cjs +10 -2
  48. package/get-shit-done/bin/lib/config.cjs +71 -37
  49. package/get-shit-done/bin/lib/core.cjs +70 -8
  50. package/get-shit-done/bin/lib/gsd2-import.cjs +511 -0
  51. package/get-shit-done/bin/lib/init.cjs +20 -6
  52. package/get-shit-done/bin/lib/intel.cjs +660 -0
  53. package/get-shit-done/bin/lib/learnings.cjs +378 -0
  54. package/get-shit-done/bin/lib/milestone.cjs +25 -15
  55. package/get-shit-done/bin/lib/model-profiles.cjs +17 -17
  56. package/get-shit-done/bin/lib/phase.cjs +148 -112
  57. package/get-shit-done/bin/lib/roadmap.cjs +12 -5
  58. package/get-shit-done/bin/lib/security.cjs +119 -0
  59. package/get-shit-done/bin/lib/state.cjs +283 -221
  60. package/get-shit-done/bin/lib/template.cjs +8 -4
  61. package/get-shit-done/bin/lib/verify.cjs +42 -5
  62. package/get-shit-done/references/ai-evals.md +156 -0
  63. package/get-shit-done/references/ai-frameworks.md +186 -0
  64. package/get-shit-done/references/common-bug-patterns.md +114 -0
  65. package/get-shit-done/references/few-shot-examples/plan-checker.md +73 -0
  66. package/get-shit-done/references/few-shot-examples/verifier.md +109 -0
  67. package/get-shit-done/references/gates.md +70 -0
  68. package/get-shit-done/references/ios-scaffold.md +123 -0
  69. package/get-shit-done/references/model-profile-resolution.md +6 -7
  70. package/get-shit-done/references/model-profiles.md +20 -14
  71. package/get-shit-done/references/planning-config.md +237 -0
  72. package/get-shit-done/references/thinking-models-debug.md +44 -0
  73. package/get-shit-done/references/thinking-models-execution.md +50 -0
  74. package/get-shit-done/references/thinking-models-planning.md +62 -0
  75. package/get-shit-done/references/thinking-models-research.md +50 -0
  76. package/get-shit-done/references/thinking-models-verification.md +55 -0
  77. package/get-shit-done/references/thinking-partner.md +96 -0
  78. package/get-shit-done/references/universal-anti-patterns.md +6 -1
  79. package/get-shit-done/references/verification-overrides.md +227 -0
  80. package/get-shit-done/templates/AI-SPEC.md +246 -0
  81. package/get-shit-done/workflows/add-tests.md +3 -0
  82. package/get-shit-done/workflows/add-todo.md +2 -0
  83. package/get-shit-done/workflows/ai-integration-phase.md +284 -0
  84. package/get-shit-done/workflows/audit-fix.md +154 -0
  85. package/get-shit-done/workflows/autonomous.md +33 -2
  86. package/get-shit-done/workflows/check-todos.md +2 -0
  87. package/get-shit-done/workflows/cleanup.md +2 -0
  88. package/get-shit-done/workflows/code-review-fix.md +497 -0
  89. package/get-shit-done/workflows/code-review.md +515 -0
  90. package/get-shit-done/workflows/complete-milestone.md +40 -15
  91. package/get-shit-done/workflows/diagnose-issues.md +1 -1
  92. package/get-shit-done/workflows/discovery-phase.md +3 -1
  93. package/get-shit-done/workflows/discuss-phase-assumptions.md +1 -1
  94. package/get-shit-done/workflows/discuss-phase.md +21 -7
  95. package/get-shit-done/workflows/do.md +2 -0
  96. package/get-shit-done/workflows/docs-update.md +2 -0
  97. package/get-shit-done/workflows/eval-review.md +155 -0
  98. package/get-shit-done/workflows/execute-phase.md +307 -57
  99. package/get-shit-done/workflows/execute-plan.md +64 -93
  100. package/get-shit-done/workflows/explore.md +136 -0
  101. package/get-shit-done/workflows/help.md +1 -1
  102. package/get-shit-done/workflows/import.md +273 -0
  103. package/get-shit-done/workflows/inbox.md +387 -0
  104. package/get-shit-done/workflows/manager.md +4 -10
  105. package/get-shit-done/workflows/new-milestone.md +3 -1
  106. package/get-shit-done/workflows/new-project.md +2 -0
  107. package/get-shit-done/workflows/new-workspace.md +2 -0
  108. package/get-shit-done/workflows/next.md +56 -0
  109. package/get-shit-done/workflows/note.md +2 -0
  110. package/get-shit-done/workflows/plan-phase.md +97 -17
  111. package/get-shit-done/workflows/plant-seed.md +3 -0
  112. package/get-shit-done/workflows/pr-branch.md +41 -13
  113. package/get-shit-done/workflows/profile-user.md +4 -2
  114. package/get-shit-done/workflows/quick.md +99 -4
  115. package/get-shit-done/workflows/remove-workspace.md +2 -0
  116. package/get-shit-done/workflows/review.md +53 -6
  117. package/get-shit-done/workflows/scan.md +98 -0
  118. package/get-shit-done/workflows/secure-phase.md +2 -0
  119. package/get-shit-done/workflows/settings.md +18 -3
  120. package/get-shit-done/workflows/ship.md +3 -0
  121. package/get-shit-done/workflows/ui-phase.md +10 -2
  122. package/get-shit-done/workflows/ui-review.md +2 -0
  123. package/get-shit-done/workflows/undo.md +314 -0
  124. package/get-shit-done/workflows/update.md +2 -0
  125. package/get-shit-done/workflows/validate-phase.md +2 -0
  126. package/get-shit-done/workflows/verify-phase.md +83 -0
  127. package/get-shit-done/workflows/verify-work.md +12 -1
  128. package/package.json +1 -1
  129. package/skills/gsd-code-review/SKILL.md +48 -0
  130. package/skills/gsd-code-review-fix/SKILL.md +44 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd-thread
3
3
  description: Manage persistent context threads for cross-session work
4
- argument-hint: [name | description]
4
+ argument-hint: "[name | description]"
5
5
  permissions:
6
6
  read: true
7
7
  write: true
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: gsd-undo
3
+ description: "Safe git revert. Roll back phase or plan commits using the phase manifest with dependency checks."
4
+ argument-hint: "--last N | --phase NN | --plan NN-MM"
5
+ permissions:
6
+ read: true
7
+ bash: true
8
+ glob: true
9
+ grep: true
10
+ question: true
11
+ ---
12
+
13
+ <objective>
14
+ Safe git revert — roll back GSD phase or plan commits using the phase manifest, with dependency checks and a confirmation gate before execution.
15
+
16
+ Three modes:
17
+ - **--last N**: Show recent GSD commits for interactive selection
18
+ - **--phase NN**: Revert all commits for a phase (manifest + git log fallback)
19
+ - **--plan NN-MM**: Revert all commits for a specific plan
20
+ </objective>
21
+
22
+ <execution_context>
23
+ @$HOME/.config/opencode/get-shit-done/workflows/undo.md
24
+ @$HOME/.config/opencode/get-shit-done/references/ui-brand.md
25
+ @$HOME/.config/opencode/get-shit-done/references/gate-prompts.md
26
+ </execution_context>
27
+
28
+ <context>
29
+ $ARGUMENTS
30
+ </context>
31
+
32
+ <process>
33
+ Execute the undo workflow from @$HOME/.config/opencode/get-shit-done/workflows/undo.md end-to-end.
34
+ </process>
@@ -36,30 +36,30 @@ If no subcommand given, default to `list`.
36
36
  ## Step 2: Execute Operation
37
37
 
38
38
  ### list
39
- Run: `node "$GSD_TOOLS" workstream list --raw --cwd "$CWD"`
39
+ Run: `node "$HOME/.config/opencode/get-shit-done/bin/gsd-tools.cjs" workstream list --raw --cwd "$CWD"`
40
40
  Display the workstreams in a table format showing name, status, current phase, and progress.
41
41
 
42
42
  ### create
43
- Run: `node "$GSD_TOOLS" workstream create <name> --raw --cwd "$CWD"`
43
+ Run: `node "$HOME/.config/opencode/get-shit-done/bin/gsd-tools.cjs" workstream create <name> --raw --cwd "$CWD"`
44
44
  After creation, display the new workstream path and suggest next steps:
45
45
  - `/gsd-new-milestone --ws <name>` to set up the milestone
46
46
 
47
47
  ### status
48
- Run: `node "$GSD_TOOLS" workstream status <name> --raw --cwd "$CWD"`
48
+ Run: `node "$HOME/.config/opencode/get-shit-done/bin/gsd-tools.cjs" workstream status <name> --raw --cwd "$CWD"`
49
49
  Display detailed phase breakdown and state information.
50
50
 
51
51
  ### switch
52
- Run: `node "$GSD_TOOLS" workstream set <name> --raw --cwd "$CWD"`
52
+ Run: `node "$HOME/.config/opencode/get-shit-done/bin/gsd-tools.cjs" workstream set <name> --raw --cwd "$CWD"`
53
53
  Also set `GSD_WORKSTREAM` for the current session when the runtime supports it.
54
54
  If the runtime exposes a session identifier, GSD also stores the active workstream
55
55
  session-locally so concurrent sessions do not overwrite each other.
56
56
 
57
57
  ### progress
58
- Run: `node "$GSD_TOOLS" workstream progress --raw --cwd "$CWD"`
58
+ Run: `node "$HOME/.config/opencode/get-shit-done/bin/gsd-tools.cjs" workstream progress --raw --cwd "$CWD"`
59
59
  Display a progress overview across all workstreams.
60
60
 
61
61
  ### complete
62
- Run: `node "$GSD_TOOLS" workstream complete <name> --raw --cwd "$CWD"`
62
+ Run: `node "$HOME/.config/opencode/get-shit-done/bin/gsd-tools.cjs" workstream complete <name> --raw --cwd "$CWD"`
63
63
  Archive the workstream to milestones/.
64
64
 
65
65
  ### resume
@@ -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
- await runCommand(command, args, cwd, raw);
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 filesToStage = files && files.length > 0 ? files : ['.planning/'];
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
- // File was deleted/moved — stage the deletion
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, planningRoot, CONFIG_DEFAULTS } = require('./core.cjs');
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 = planningRoot(cwd);
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
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
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 = planningRoot(cwd);
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
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
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(planningRoot(cwd), 'config.json');
299
+ const configPath = path.join(planningDir(cwd), 'config.json');
279
300
 
280
- // Load existing config or start with empty object
281
- let config = {};
282
- try {
283
- if (fs.existsSync(configPath)) {
284
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
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
- // Set nested value using dot notation (e.g., "workflow.research")
291
- const keys = keyPath.split('.');
292
- let current = config;
293
- for (let i = 0; i < keys.length - 1; i++) {
294
- const key = keys[i];
295
- if (current[key] === undefined || typeof current[key] !== 'object') {
296
- current[key] = {};
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
- current = current[key];
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
- // write back
304
- try {
305
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
306
- return { updated: true, key: keyPath, value: parsedValue, previousValue };
307
- } catch (err) {
308
- error('Failed to write config.json: ' + err.message);
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(planningRoot(cwd), 'config.json');
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