@opengsd/gsd-core 1.7.0 → 1.8.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 (165) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +14 -0
  4. package/README.md +2 -0
  5. package/agents/gsd-debug-session-manager.md +42 -4
  6. package/agents/gsd-debugger.md +87 -29
  7. package/agents/gsd-executor.md +29 -2
  8. package/agents/gsd-planner.md +29 -36
  9. package/agents/gsd-verifier.md +2 -2
  10. package/bin/install.js +1152 -80
  11. package/commands/gsd/ai-integration-phase.md +1 -1
  12. package/commands/gsd/mempalace-capture.md +9 -5
  13. package/commands/gsd/new-milestone.md +1 -1
  14. package/commands/gsd/plan-phase.md +5 -3
  15. package/commands/gsd/plan-review-convergence.md +3 -2
  16. package/gsd-core/bin/gsd-tools.cjs +1878 -2507
  17. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  18. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  19. package/gsd-core/bin/lib/api-coverage.cjs +338 -45
  20. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  21. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  22. package/gsd-core/bin/lib/capability-registry.cjs +155 -86
  23. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  24. package/gsd-core/bin/lib/check-command-router.cjs +128 -25
  25. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
  26. package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
  27. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  28. package/gsd-core/bin/lib/commands.cjs +81 -4
  29. package/gsd-core/bin/lib/config-loader.cjs +14 -2
  30. package/gsd-core/bin/lib/config.cjs +69 -18
  31. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  32. package/gsd-core/bin/lib/decisions.cjs +32 -8
  33. package/gsd-core/bin/lib/docs.cjs +6 -0
  34. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  35. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  36. package/gsd-core/bin/lib/init.cjs +111 -47
  37. package/gsd-core/bin/lib/install-engine.cjs +298 -23
  38. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  39. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  40. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  41. package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
  42. package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
  43. package/gsd-core/bin/lib/milestone.cjs +246 -12
  44. package/gsd-core/bin/lib/model-catalog.cjs +19 -4
  45. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  46. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  47. package/gsd-core/bin/lib/phase-id.cjs +26 -4
  48. package/gsd-core/bin/lib/phase.cjs +201 -12
  49. package/gsd-core/bin/lib/plan-scan.cjs +70 -2
  50. package/gsd-core/bin/lib/roadmap-parser.cjs +7 -4
  51. package/gsd-core/bin/lib/roadmap.cjs +13 -3
  52. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -1
  53. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -8
  54. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +16 -0
  55. package/gsd-core/bin/lib/smart-entry.cjs +69 -4
  56. package/gsd-core/bin/lib/state-document.cjs +7 -4
  57. package/gsd-core/bin/lib/state-transition.cjs +22 -1
  58. package/gsd-core/bin/lib/state.cjs +65 -11
  59. package/gsd-core/bin/lib/surface.cjs +51 -9
  60. package/gsd-core/bin/lib/uat.cjs +420 -5
  61. package/gsd-core/bin/lib/validate.cjs +12 -8
  62. package/gsd-core/bin/lib/verification.cjs +112 -17
  63. package/gsd-core/bin/lib/verify.cjs +220 -22
  64. package/gsd-core/bin/shared/config-schema.manifest.json +3 -2
  65. package/gsd-core/references/api-coverage.md +37 -7
  66. package/gsd-core/references/checkpoints.md +1 -1
  67. package/gsd-core/references/common-bug-patterns.md +13 -0
  68. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  69. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  70. package/gsd-core/references/debugger-philosophy.md +1 -0
  71. package/gsd-core/references/debugger-prevention.md +98 -0
  72. package/gsd-core/references/debugger-rca-branching.md +98 -0
  73. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  74. package/gsd-core/references/debugger-sbfl.md +110 -0
  75. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  76. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  77. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  78. package/gsd-core/references/execute-phase-response-language.md +7 -0
  79. package/gsd-core/references/planner-antipatterns.md +6 -0
  80. package/gsd-core/references/planner-mvp-mode.md +12 -13
  81. package/gsd-core/references/planner-preconditions.md +156 -0
  82. package/gsd-core/references/planner-reversibility.md +132 -0
  83. package/gsd-core/references/reviewer-instances.md +9 -7
  84. package/gsd-core/references/skeleton-template.md +1 -1
  85. package/gsd-core/references/thinking-models-planning.md +3 -1
  86. package/gsd-core/templates/DEBUG.md +5 -3
  87. package/gsd-core/workflows/add-phase.md +2 -0
  88. package/gsd-core/workflows/add-tests.md +3 -1
  89. package/gsd-core/workflows/add-todo.md +32 -1
  90. package/gsd-core/workflows/ai-integration-phase.md +4 -2
  91. package/gsd-core/workflows/audit-fix.md +2 -2
  92. package/gsd-core/workflows/check-todos.md +3 -1
  93. package/gsd-core/workflows/cleanup.md +7 -1
  94. package/gsd-core/workflows/code-review.md +17 -5
  95. package/gsd-core/workflows/complete-milestone.md +3 -0
  96. package/gsd-core/workflows/debug.md +25 -5
  97. package/gsd-core/workflows/diagnose-issues.md +1 -1
  98. package/gsd-core/workflows/discovery-phase.md +7 -0
  99. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  100. package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
  101. package/gsd-core/workflows/do.md +7 -1
  102. package/gsd-core/workflows/docs-update.md +1 -0
  103. package/gsd-core/workflows/eval-review.md +3 -0
  104. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  105. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  106. package/gsd-core/workflows/execute-phase.md +25 -34
  107. package/gsd-core/workflows/execute-plan.md +15 -4
  108. package/gsd-core/workflows/graduation.md +3 -0
  109. package/gsd-core/workflows/health.md +7 -1
  110. package/gsd-core/workflows/help/modes/full.md +6 -2
  111. package/gsd-core/workflows/import.md +8 -2
  112. package/gsd-core/workflows/inbox.md +7 -0
  113. package/gsd-core/workflows/ingest-docs.md +15 -10
  114. package/gsd-core/workflows/manager.md +3 -1
  115. package/gsd-core/workflows/map-codebase.md +4 -4
  116. package/gsd-core/workflows/mvp-phase.md +3 -0
  117. package/gsd-core/workflows/new-milestone.md +69 -21
  118. package/gsd-core/workflows/new-project.md +17 -15
  119. package/gsd-core/workflows/new-workspace.md +3 -1
  120. package/gsd-core/workflows/onboard.md +3 -0
  121. package/gsd-core/workflows/plan-phase.md +14 -5
  122. package/gsd-core/workflows/plan-review-convergence.md +48 -3
  123. package/gsd-core/workflows/plant-seed.md +3 -0
  124. package/gsd-core/workflows/profile-user.md +7 -1
  125. package/gsd-core/workflows/progress.md +31 -3
  126. package/gsd-core/workflows/quick.md +19 -7
  127. package/gsd-core/workflows/remove-workspace.md +3 -0
  128. package/gsd-core/workflows/review.md +89 -73
  129. package/gsd-core/workflows/scan.md +1 -1
  130. package/gsd-core/workflows/secure-phase.md +3 -0
  131. package/gsd-core/workflows/settings-integrations.md +3 -0
  132. package/gsd-core/workflows/settings.md +3 -0
  133. package/gsd-core/workflows/ship.md +50 -3
  134. package/gsd-core/workflows/sketch.md +3 -0
  135. package/gsd-core/workflows/smart-entry.md +3 -0
  136. package/gsd-core/workflows/spike.md +7 -1
  137. package/gsd-core/workflows/ui-phase.md +3 -1
  138. package/gsd-core/workflows/ui-review.md +3 -0
  139. package/gsd-core/workflows/undo.md +7 -0
  140. package/gsd-core/workflows/update.md +2 -0
  141. package/gsd-core/workflows/validate-phase.md +3 -0
  142. package/gsd-core/workflows/verify-phase.md +2 -2
  143. package/gsd-core/workflows/verify-work.md +7 -3
  144. package/hooks/dist/gsd-context-monitor.js +27 -9
  145. package/hooks/dist/gsd-statusline.js +88 -3
  146. package/hooks/gsd-context-monitor.js +27 -9
  147. package/hooks/gsd-statusline.js +88 -3
  148. package/package.json +6 -4
  149. package/pi/gsd.cjs +8 -2
  150. package/scripts/changeset/lint.cjs +1 -0
  151. package/scripts/changeset/parse.cjs +26 -0
  152. package/scripts/check-glossary-refs.cjs +220 -0
  153. package/scripts/ci-rebase-check.cjs +48 -4
  154. package/scripts/gen-adr-index.cjs +526 -0
  155. package/scripts/gen-test-timings.cjs +201 -0
  156. package/scripts/lint-portable-timeout.cjs +140 -0
  157. package/scripts/lint-test-file-count.allowlist.json +1 -0
  158. package/scripts/release-tarball-smoke.cjs +18 -11
  159. package/scripts/run-tests.cjs +420 -58
  160. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  161. package/skills/gsd-mempalace-capture/SKILL.md +9 -5
  162. package/skills/gsd-new-milestone/SKILL.md +1 -1
  163. package/skills/gsd-plan-phase/SKILL.md +5 -3
  164. package/skills/gsd-plan-review-convergence/SKILL.md +3 -2
  165. package/vscode/package.json +1 -1
@@ -11,6 +11,7 @@ const os = require('os');
11
11
  const childProcess = require('child_process');
12
12
  const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
13
13
  const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
14
+ const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
14
15
 
15
16
  // --- Config + last-command readers ------------------------------------------
16
17
 
@@ -319,6 +320,78 @@ function contextTokenSuffix(currentUsage) {
319
320
  return total > 0 ? ` (${formatTokens(total)})` : '';
320
321
  }
321
322
 
323
+ // --- Compact state format (opt-in) ---------------------------------------------
324
+
325
+ /**
326
+ * Collapse GSD's free-text status (often a multi-sentence narrative) to a
327
+ * single keyword, built on the canonical normalizer (#2162 approval
328
+ * condition): normalizeStateStatus() in state-document.cjs owns the status
329
+ * vocabulary (discussing / planning / executing / verifying / completed /
330
+ * paused) so the two can't drift. "paused" — the canonical stuck state — is
331
+ * uppercased to PAUSED, the one state worth shouting about. Statuses the
332
+ * normalizer passes through unrecognized fall back to their first word,
333
+ * capped at 16 chars so a rogue STATE.md can't blow up the line.
334
+ * Returns null for empty input.
335
+ */
336
+ const CANONICAL_STATUSES = ['discussing', 'planning', 'executing', 'verifying', 'completed', 'paused'];
337
+
338
+ function shortGsdStatus(status) {
339
+ if (!status) return null;
340
+ const norm = normalizeStateStatus(status, null);
341
+ if (CANONICAL_STATUSES.includes(norm)) {
342
+ return norm === 'paused' ? 'PAUSED' : norm;
343
+ }
344
+ // Unrecognized free text passes through normalizeStateStatus verbatim —
345
+ // fall back to the first word, capped.
346
+ const first = String(norm).trim().split(/[\s\u2014\u2013-]+/)[0] || '';
347
+ return first ? first.slice(0, 16) : null;
348
+ }
349
+
350
+ /**
351
+ * Compact alternative to formatGsdState, selected via
352
+ * `statusline.state_format: "compact"`:
353
+ *
354
+ * "v1.12 · P7/12 · executing" (phase active)
355
+ * "v2.0 · P4.5 · BLOCKED" (no total known)
356
+ * "v2.0 · complete" (milestone done)
357
+ * "v2.0 · next execute-phase 4.5" (idle with a queued action)
358
+ *
359
+ * Drops the milestone name and progress bar — the biggest width costs in the
360
+ * default format — and collapses narrative statuses via shortGsdStatus().
361
+ * The default "full" format is untouched.
362
+ */
363
+ function formatGsdStateCompact(s) {
364
+ const parts = [];
365
+
366
+ if (s.milestone) parts.push(s.milestone);
367
+
368
+ const phaseId = s.activePhase || s.phaseNum;
369
+ if (phaseId) {
370
+ parts.push(s.phaseTotal ? `P${phaseId}/${s.phaseTotal}` : `P${phaseId}`);
371
+ }
372
+
373
+ // Scene exclusivity mirrors formatGsdState's if/else chain: an in-flight
374
+ // phase (Scene 1, gated on activePhase ONLY — the legacy phaseNum shape
375
+ // still completes) wins over milestone-complete (Scene 3), even if a
376
+ // non-atomic STATE.md edit leaves percent=100 alongside a lifecycle phase.
377
+ const done = !s.activePhase && (Number(s.percent) === 100 ||
378
+ (s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases));
379
+
380
+ if (done) {
381
+ parts.push('complete');
382
+ } else {
383
+ const st = shortGsdStatus(s.status);
384
+ if (st) {
385
+ parts.push(st);
386
+ } else if (!phaseId && s.nextAction) {
387
+ const phasesStr = (s.nextPhases && s.nextPhases.length > 0) ? s.nextPhases.join('/') : '';
388
+ parts.push(`next ${s.nextAction}${phasesStr ? ' ' + phasesStr : ''}`);
389
+ }
390
+ }
391
+
392
+ return parts.join(' \u00b7 ');
393
+ }
394
+
322
395
  // --- Model name --------------------------------------------------------------
323
396
 
324
397
  /**
@@ -529,8 +602,9 @@ function runStatusline() {
529
602
  }
530
603
  }
531
604
 
532
- // GSD state (milestone · status · phase) — shown when no todo task
533
- const gsdStateStr = task ? '' : formatGsdState(readGsdState(dir) || {});
605
+ // GSD state (milestone · status · phase) — shown when no todo task.
606
+ // Format resolved below once config is read (statusline.state_format).
607
+ let gsdStateStr = '';
534
608
 
535
609
  // GSD update available?
536
610
  // Read only the per-package shared cache file (#607). The legacy
@@ -558,6 +632,7 @@ function runStatusline() {
558
632
  // Failure here must never break the statusline — wrap the entire lookup.
559
633
  let lastCmdSuffix = '';
560
634
  let position = 'end';
635
+ let stateFormat = 'full';
561
636
  let gitSuffix = '';
562
637
  try {
563
638
  if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
@@ -569,6 +644,7 @@ function runStatusline() {
569
644
  }
570
645
  const cfgPos = getConfigValue(cfg, 'statusline.context_position');
571
646
  if (cfgPos != null) position = cfgPos;
647
+ if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
572
648
  if (getConfigValue(cfg, 'statusline.show_git') === true) {
573
649
  gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
574
650
  }
@@ -576,6 +652,11 @@ function runStatusline() {
576
652
  // Never break the statusline on config/transcript/git errors
577
653
  }
578
654
 
655
+ if (!task) {
656
+ const state = readGsdState(dir) || {};
657
+ gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
658
+ }
659
+
579
660
  // Output
580
661
  const dirname = path.basename(dir);
581
662
  const middle = task
@@ -675,6 +756,7 @@ module.exports = {
675
756
  evaluateUpdateCache,
676
757
  formatTokens,
677
758
  contextTokenSuffix,
759
+ shortGsdStatus, formatGsdStateCompact,
678
760
  compactModelName,
679
761
  readGitStatus, parseGitStatus, buildGitSegment,
680
762
  };
@@ -690,6 +772,7 @@ function renderStatusline(data) {
690
772
 
691
773
  let lastCmdSuffix = '';
692
774
  let position = 'end';
775
+ let stateFormat = 'full';
693
776
  let gitSuffix = '';
694
777
  try {
695
778
  const cfg = readGsdConfig(dir);
@@ -701,12 +784,14 @@ function renderStatusline(data) {
701
784
  }
702
785
  const cfgPos = getConfigValue(cfg, 'statusline.context_position');
703
786
  if (cfgPos != null) position = cfgPos;
787
+ if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
704
788
  if (getConfigValue(cfg, 'statusline.show_git') === true) {
705
789
  gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
706
790
  }
707
791
  } catch (e) { /* swallow */ }
708
792
 
709
- const gsdStateStr = formatGsdState(readGsdState(dir) || {});
793
+ const state = readGsdState(dir) || {};
794
+ const gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
710
795
  const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
711
796
  return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position });
712
797
  }
@@ -180,15 +180,33 @@ process.stdin.on('end', () => {
180
180
  'starting new complex work.';
181
181
  }
182
182
 
183
- const output = {
184
- hookSpecificOutput: {
185
- hookEventName: (data.hook_event_name && data.hook_event_name.trim())
186
- || (process.env.GEMINI_API_KEY ? "AfterTool" : "PostToolUse"),
187
- additionalContext: message
188
- }
189
- };
190
-
191
- process.stdout.write(JSON.stringify(output));
183
+ // #2289: the hookSpecificOutput.additionalContext envelope is only a valid
184
+ // output shape for the context-injection events (PostToolUse, and AfterTool
185
+ // for the Gemini dialect). This hook is also wired to other lifecycle events
186
+ // on some hosts — Codex registers it under Stop / SubagentStart /
187
+ // SubagentStop / PreCompact (#772) — and those reject the envelope
188
+ // ("hook returned invalid stop hook JSON output"). Use a POSITIVE allowlist:
189
+ // emit only for injection-capable events; every other event, and a
190
+ // missing/unrecognized name, exits 0 with no stdout. A Stop-only blacklist is
191
+ // not enough — a missing name would still fall through to the injection path.
192
+ // All side effects above (debounce counter, one-time critical-session
193
+ // recording) have already run regardless of whether output is emitted.
194
+ const eventName = (data.hook_event_name && data.hook_event_name.trim()) || "";
195
+ // Preserve the pre-#2289 Gemini fallback: a missing event name under a
196
+ // Gemini-dialect runtime (GEMINI_API_KEY set) still means AfterTool, so its
197
+ // advisory output is unchanged. A missing name on any other host is silent.
198
+ const geminiFallback = eventName === "" && !!process.env.GEMINI_API_KEY;
199
+ const injectionSupported = eventName === "PostToolUse" || eventName === "AfterTool" || geminiFallback;
200
+
201
+ if (injectionSupported) {
202
+ const output = {
203
+ hookSpecificOutput: {
204
+ hookEventName: eventName || "AfterTool",
205
+ additionalContext: message
206
+ }
207
+ };
208
+ process.stdout.write(JSON.stringify(output));
209
+ }
192
210
  } catch (e) {
193
211
  // Silent fail -- never block tool execution
194
212
  process.exit(0);
@@ -11,6 +11,7 @@ const os = require('os');
11
11
  const childProcess = require('child_process');
12
12
  const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
13
13
  const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
14
+ const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
14
15
 
15
16
  // --- Config + last-command readers ------------------------------------------
16
17
 
@@ -319,6 +320,78 @@ function contextTokenSuffix(currentUsage) {
319
320
  return total > 0 ? ` (${formatTokens(total)})` : '';
320
321
  }
321
322
 
323
+ // --- Compact state format (opt-in) ---------------------------------------------
324
+
325
+ /**
326
+ * Collapse GSD's free-text status (often a multi-sentence narrative) to a
327
+ * single keyword, built on the canonical normalizer (#2162 approval
328
+ * condition): normalizeStateStatus() in state-document.cjs owns the status
329
+ * vocabulary (discussing / planning / executing / verifying / completed /
330
+ * paused) so the two can't drift. "paused" — the canonical stuck state — is
331
+ * uppercased to PAUSED, the one state worth shouting about. Statuses the
332
+ * normalizer passes through unrecognized fall back to their first word,
333
+ * capped at 16 chars so a rogue STATE.md can't blow up the line.
334
+ * Returns null for empty input.
335
+ */
336
+ const CANONICAL_STATUSES = ['discussing', 'planning', 'executing', 'verifying', 'completed', 'paused'];
337
+
338
+ function shortGsdStatus(status) {
339
+ if (!status) return null;
340
+ const norm = normalizeStateStatus(status, null);
341
+ if (CANONICAL_STATUSES.includes(norm)) {
342
+ return norm === 'paused' ? 'PAUSED' : norm;
343
+ }
344
+ // Unrecognized free text passes through normalizeStateStatus verbatim —
345
+ // fall back to the first word, capped.
346
+ const first = String(norm).trim().split(/[\s\u2014\u2013-]+/)[0] || '';
347
+ return first ? first.slice(0, 16) : null;
348
+ }
349
+
350
+ /**
351
+ * Compact alternative to formatGsdState, selected via
352
+ * `statusline.state_format: "compact"`:
353
+ *
354
+ * "v1.12 · P7/12 · executing" (phase active)
355
+ * "v2.0 · P4.5 · BLOCKED" (no total known)
356
+ * "v2.0 · complete" (milestone done)
357
+ * "v2.0 · next execute-phase 4.5" (idle with a queued action)
358
+ *
359
+ * Drops the milestone name and progress bar — the biggest width costs in the
360
+ * default format — and collapses narrative statuses via shortGsdStatus().
361
+ * The default "full" format is untouched.
362
+ */
363
+ function formatGsdStateCompact(s) {
364
+ const parts = [];
365
+
366
+ if (s.milestone) parts.push(s.milestone);
367
+
368
+ const phaseId = s.activePhase || s.phaseNum;
369
+ if (phaseId) {
370
+ parts.push(s.phaseTotal ? `P${phaseId}/${s.phaseTotal}` : `P${phaseId}`);
371
+ }
372
+
373
+ // Scene exclusivity mirrors formatGsdState's if/else chain: an in-flight
374
+ // phase (Scene 1, gated on activePhase ONLY — the legacy phaseNum shape
375
+ // still completes) wins over milestone-complete (Scene 3), even if a
376
+ // non-atomic STATE.md edit leaves percent=100 alongside a lifecycle phase.
377
+ const done = !s.activePhase && (Number(s.percent) === 100 ||
378
+ (s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases));
379
+
380
+ if (done) {
381
+ parts.push('complete');
382
+ } else {
383
+ const st = shortGsdStatus(s.status);
384
+ if (st) {
385
+ parts.push(st);
386
+ } else if (!phaseId && s.nextAction) {
387
+ const phasesStr = (s.nextPhases && s.nextPhases.length > 0) ? s.nextPhases.join('/') : '';
388
+ parts.push(`next ${s.nextAction}${phasesStr ? ' ' + phasesStr : ''}`);
389
+ }
390
+ }
391
+
392
+ return parts.join(' \u00b7 ');
393
+ }
394
+
322
395
  // --- Model name --------------------------------------------------------------
323
396
 
324
397
  /**
@@ -529,8 +602,9 @@ function runStatusline() {
529
602
  }
530
603
  }
531
604
 
532
- // GSD state (milestone · status · phase) — shown when no todo task
533
- const gsdStateStr = task ? '' : formatGsdState(readGsdState(dir) || {});
605
+ // GSD state (milestone · status · phase) — shown when no todo task.
606
+ // Format resolved below once config is read (statusline.state_format).
607
+ let gsdStateStr = '';
534
608
 
535
609
  // GSD update available?
536
610
  // Read only the per-package shared cache file (#607). The legacy
@@ -558,6 +632,7 @@ function runStatusline() {
558
632
  // Failure here must never break the statusline — wrap the entire lookup.
559
633
  let lastCmdSuffix = '';
560
634
  let position = 'end';
635
+ let stateFormat = 'full';
561
636
  let gitSuffix = '';
562
637
  try {
563
638
  if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
@@ -569,6 +644,7 @@ function runStatusline() {
569
644
  }
570
645
  const cfgPos = getConfigValue(cfg, 'statusline.context_position');
571
646
  if (cfgPos != null) position = cfgPos;
647
+ if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
572
648
  if (getConfigValue(cfg, 'statusline.show_git') === true) {
573
649
  gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
574
650
  }
@@ -576,6 +652,11 @@ function runStatusline() {
576
652
  // Never break the statusline on config/transcript/git errors
577
653
  }
578
654
 
655
+ if (!task) {
656
+ const state = readGsdState(dir) || {};
657
+ gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
658
+ }
659
+
579
660
  // Output
580
661
  const dirname = path.basename(dir);
581
662
  const middle = task
@@ -675,6 +756,7 @@ module.exports = {
675
756
  evaluateUpdateCache,
676
757
  formatTokens,
677
758
  contextTokenSuffix,
759
+ shortGsdStatus, formatGsdStateCompact,
678
760
  compactModelName,
679
761
  readGitStatus, parseGitStatus, buildGitSegment,
680
762
  };
@@ -690,6 +772,7 @@ function renderStatusline(data) {
690
772
 
691
773
  let lastCmdSuffix = '';
692
774
  let position = 'end';
775
+ let stateFormat = 'full';
693
776
  let gitSuffix = '';
694
777
  try {
695
778
  const cfg = readGsdConfig(dir);
@@ -701,12 +784,14 @@ function renderStatusline(data) {
701
784
  }
702
785
  const cfgPos = getConfigValue(cfg, 'statusline.context_position');
703
786
  if (cfgPos != null) position = cfgPos;
787
+ if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
704
788
  if (getConfigValue(cfg, 'statusline.show_git') === true) {
705
789
  gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
706
790
  }
707
791
  } catch (e) { /* swallow */ }
708
792
 
709
- const gsdStateStr = formatGsdState(readGsdState(dir) || {});
793
+ const state = readGsdState(dir) || {};
794
+ const gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
710
795
  const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
711
796
  return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position });
712
797
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
5
5
  "main": ".opencode/plugins/gsd-core.js",
6
6
  "bin": {
@@ -70,7 +70,9 @@
70
70
  "typescript-eslint": "^8.60.0"
71
71
  },
72
72
  "overrides": {
73
- "qs": ">=6.15.2"
73
+ "qs": ">=6.15.2",
74
+ "body-parser": ">=2.3.0",
75
+ "@hono/node-server": ">=2.0.5"
74
76
  },
75
77
  "optionalDependencies": {
76
78
  "fallow": "^2.70.0"
@@ -101,7 +103,7 @@
101
103
  "lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
102
104
  "lint:fix": "eslint . --fix",
103
105
  "lint:table-schema-drift": "node scripts/lint-table-schema-drift.cjs",
104
- "lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs",
106
+ "lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/lint-portable-timeout.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs",
105
107
  "lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
106
108
  "lint:regression-names": "node scripts/lint-regression-test-names.cjs",
107
109
  "lint:descriptions": "node scripts/lint-descriptions.cjs",
@@ -109,7 +111,7 @@
109
111
  "lint:test-file-count": "node scripts/lint-test-file-count.cjs",
110
112
  "lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
111
113
  "lint:changeset": "node scripts/changeset/lint.cjs",
112
- "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check",
114
+ "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/check-glossary-refs.cjs --check",
113
115
  "lint:docs": "node scripts/lint-docs-required.cjs",
114
116
  "lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
115
117
  "ci:test-scope": "node scripts/ci-test-scope.cjs",
package/pi/gsd.cjs CHANGED
@@ -9,8 +9,14 @@
9
9
  * This extension binds GSD's command surface to pi via the imperative adapter
10
10
  * path — the programmatic-CLI peer of the OpenCode worked binding.
11
11
  *
12
- * Installation: copy this file to ~/.pi/agent/extensions/gsd.cjs (pi loads
13
- * extensions via jiti from that dir). The engine is resolved from the installed
12
+ * Installation: copy this file to ~/.pi/agent/extensions/gsd.js — note the
13
+ * `.js` DEST suffix, not `.cjs`. pi auto-discovers extensions/ entries through
14
+ * `isExtensionFile()`, which accepts only `.ts`/`.js` and skips anything else
15
+ * SILENTLY (no error, no log line), so a `.cjs` dest installs fine and is then
16
+ * never loaded (#2470). This source file keeps its `.cjs` suffix on purpose —
17
+ * tests `require()` it directly and `.cjs` is unambiguous CommonJS — and pi
18
+ * loads the copied file via jiti, which handles CommonJS and ESM alike, so the
19
+ * suffix gates discovery, not parsing. The engine is resolved from the installed
14
20
  * GSD tree (walk-up like the OpenCode plugin). pi's shared hooks/ bundle
15
21
  * (hooks/*.js + hooks/lib/git-cmd.js) is installed alongside the extension —
16
22
  * capabilities/pi/capability.json does NOT set
@@ -27,6 +27,7 @@ const OPT_OUT_LABEL = 'no-changelog';
27
27
  const USER_FACING_PREFIXES = [
28
28
  'bin/',
29
29
  'gsd-core/',
30
+ 'src/',
30
31
  'agents/',
31
32
  'commands/',
32
33
  'hooks/',
@@ -65,10 +65,23 @@ function extractDocsExempt(body) {
65
65
  // is CRLF-aware so Windows-authored fragments don't leave residual `\r`
66
66
  // characters that would shift the `(#NNNN)` PR suffix to a blank line in
67
67
  // the rendered CHANGELOG.md / GitHub release-notes bullet.
68
+ //
69
+ // Both leading AND trailing line terminators are stripped. `DOCS_EXEMPT_RE`
70
+ // removes the marker's own text but its `$` anchor (multiline mode) does
71
+ // not consume the `\n` that terminates the marker's line. When the marker
72
+ // is the FIRST line of the body, that leftover `\n` becomes the new first
73
+ // character of `body` — serializeChangelog then emits an empty `- ` bullet
74
+ // followed by an orphaned continuation paragraph, and parseChangelog's
75
+ // bullet-continuation check (which requires a leading `\s`) treats that
76
+ // non-indented paragraph as terminating the bullet, silently dropping the
77
+ // entry's content on re-parse. Stripping leading terminators here closes
78
+ // that gap the same way the trailing strip already does for the opposite
79
+ // (marker-last) position.
68
80
  const cleaned = body
69
81
  .replace(DOCS_EXEMPT_RE, '')
70
82
  .replace(/[ \t\r]+$/gm, '') // strip trailing \r/spaces on each line
71
83
  .replace(/(?:\r?\n){3,}/g, '\n\n') // collapse 3+ blank lines (CRLF-aware)
84
+ .replace(/^[\r\n]+/, '') // strip terminators left by a first-line marker
72
85
  .replace(/[\r\n]+$/, ''); // strip every trailing line terminator
73
86
  return { docsExempt: reason, body: cleaned };
74
87
  }
@@ -105,6 +118,19 @@ function parseFragment(src) {
105
118
  if (body.endsWith('\r\n')) verbatimBody = body.slice(0, -2);
106
119
  else if (body.endsWith('\n')) verbatimBody = body.slice(0, -1);
107
120
  else verbatimBody = body;
121
+ // Some fragments have a blank line between the closing frontmatter `---`
122
+ // and the first line of actual content (purely a stylistic authoring
123
+ // choice — the blank line carries no significant content, unlike
124
+ // indentation inside a code block). Strip any such leading blank line(s)
125
+ // here, mirroring the trailing-terminator strip above. Without this,
126
+ // `body` starts with `\n`/`\r\n`, serializeChangelog emits an empty `- `
127
+ // bullet followed by an orphaned paragraph, and parseChangelog's
128
+ // continuation check (requires a leading `\s` on the line) treats that
129
+ // non-indented paragraph as terminating the bullet — silently dropping
130
+ // the fragment's content on re-parse. This is the same downstream failure
131
+ // mode as a first-line docs-exempt marker (see extractDocsExempt below);
132
+ // it just arises from plain authoring whitespace instead of a marker.
133
+ verbatimBody = verbatimBody.replace(/^(?:[ \t]*\r?\n)+/, '');
108
134
  const { docsExempt, body: visibleBody } = extractDocsExempt(verbatimBody);
109
135
  if (!visibleBody.trim()) return { ok: false, reason: FRAGMENT_ERROR.EMPTY_BODY };
110
136