@opengsd/gsd-core 1.7.0-rc.6 → 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 (195) 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 +31 -3
  8. package/agents/gsd-planner.md +29 -36
  9. package/agents/gsd-security-auditor.md +13 -15
  10. package/agents/gsd-verifier.md +2 -2
  11. package/bin/install.js +1157 -84
  12. package/commands/gsd/ai-integration-phase.md +1 -1
  13. package/commands/gsd/mempalace-capture.md +31 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +5 -3
  16. package/commands/gsd/plan-review-convergence.md +3 -2
  17. package/commands/gsd/surface.md +6 -6
  18. package/gsd-core/bin/gsd-tools.cjs +1866 -2434
  19. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  20. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  21. package/gsd-core/bin/lib/api-coverage.cjs +341 -49
  22. package/gsd-core/bin/lib/audit.cjs +7 -6
  23. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  24. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  25. package/gsd-core/bin/lib/capability-registry.cjs +157 -88
  26. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  27. package/gsd-core/bin/lib/check-command-router.cjs +129 -26
  28. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
  29. package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
  30. package/gsd-core/bin/lib/clock.cjs +19 -0
  31. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  32. package/gsd-core/bin/lib/commands.cjs +129 -13
  33. package/gsd-core/bin/lib/config-loader.cjs +20 -4
  34. package/gsd-core/bin/lib/config.cjs +81 -18
  35. package/gsd-core/bin/lib/core-utils.cjs +14 -3
  36. package/gsd-core/bin/lib/decisions.cjs +32 -8
  37. package/gsd-core/bin/lib/docs.cjs +6 -0
  38. package/gsd-core/bin/lib/drift.cjs +4 -4
  39. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  40. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  41. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  42. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  43. package/gsd-core/bin/lib/init.cjs +138 -60
  44. package/gsd-core/bin/lib/install-engine.cjs +301 -25
  45. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  46. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  47. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  48. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  49. package/gsd-core/bin/lib/installer-migrations.cjs +45 -6
  50. package/gsd-core/bin/lib/markdown-sectionizer.cjs +449 -0
  51. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  52. package/gsd-core/bin/lib/milestone.cjs +463 -43
  53. package/gsd-core/bin/lib/model-catalog.cjs +19 -4
  54. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  55. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  56. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  57. package/gsd-core/bin/lib/phase-id.cjs +26 -4
  58. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  59. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  60. package/gsd-core/bin/lib/phase.cjs +636 -72
  61. package/gsd-core/bin/lib/plan-scan.cjs +73 -2
  62. package/gsd-core/bin/lib/roadmap-parser.cjs +225 -17
  63. package/gsd-core/bin/lib/roadmap.cjs +113 -52
  64. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +14 -7
  65. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  66. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -9
  67. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +41 -17
  68. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  69. package/gsd-core/bin/lib/security.cjs +1 -1
  70. package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
  71. package/gsd-core/bin/lib/smart-entry.cjs +73 -7
  72. package/gsd-core/bin/lib/state-document.cjs +7 -4
  73. package/gsd-core/bin/lib/state-transition.cjs +122 -46
  74. package/gsd-core/bin/lib/state.cjs +456 -137
  75. package/gsd-core/bin/lib/surface.cjs +53 -11
  76. package/gsd-core/bin/lib/template.cjs +2 -1
  77. package/gsd-core/bin/lib/uat.cjs +474 -13
  78. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  79. package/gsd-core/bin/lib/validate.cjs +12 -8
  80. package/gsd-core/bin/lib/verification.cjs +112 -17
  81. package/gsd-core/bin/lib/verify.cjs +224 -25
  82. package/gsd-core/bin/lib/workstream.cjs +3 -2
  83. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  84. package/gsd-core/bin/lib/write-set.cjs +38 -0
  85. package/gsd-core/bin/shared/config-schema.manifest.json +5 -2
  86. package/gsd-core/references/api-coverage.md +37 -7
  87. package/gsd-core/references/checkpoints.md +13 -1
  88. package/gsd-core/references/common-bug-patterns.md +13 -0
  89. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  90. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  91. package/gsd-core/references/debugger-philosophy.md +1 -0
  92. package/gsd-core/references/debugger-prevention.md +98 -0
  93. package/gsd-core/references/debugger-rca-branching.md +98 -0
  94. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  95. package/gsd-core/references/debugger-sbfl.md +110 -0
  96. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  97. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  98. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  99. package/gsd-core/references/execute-phase-response-language.md +7 -0
  100. package/gsd-core/references/planner-antipatterns.md +6 -0
  101. package/gsd-core/references/planner-mvp-mode.md +12 -13
  102. package/gsd-core/references/planner-preconditions.md +156 -0
  103. package/gsd-core/references/planner-reversibility.md +132 -0
  104. package/gsd-core/references/reviewer-instances.md +9 -7
  105. package/gsd-core/references/skeleton-template.md +1 -1
  106. package/gsd-core/references/thinking-models-planning.md +3 -1
  107. package/gsd-core/templates/DEBUG.md +5 -3
  108. package/gsd-core/workflows/add-phase.md +2 -0
  109. package/gsd-core/workflows/add-tests.md +4 -2
  110. package/gsd-core/workflows/add-todo.md +32 -1
  111. package/gsd-core/workflows/ai-integration-phase.md +4 -2
  112. package/gsd-core/workflows/audit-fix.md +2 -2
  113. package/gsd-core/workflows/check-todos.md +3 -1
  114. package/gsd-core/workflows/cleanup.md +7 -1
  115. package/gsd-core/workflows/code-review.md +17 -5
  116. package/gsd-core/workflows/complete-milestone.md +3 -0
  117. package/gsd-core/workflows/debug.md +27 -5
  118. package/gsd-core/workflows/diagnose-issues.md +1 -1
  119. package/gsd-core/workflows/discovery-phase.md +7 -0
  120. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  121. package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
  122. package/gsd-core/workflows/do.md +7 -1
  123. package/gsd-core/workflows/docs-update.md +1 -0
  124. package/gsd-core/workflows/eval-review.md +3 -0
  125. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  126. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  127. package/gsd-core/workflows/execute-phase.md +30 -37
  128. package/gsd-core/workflows/execute-plan.md +15 -4
  129. package/gsd-core/workflows/fast.md +8 -22
  130. package/gsd-core/workflows/graduation.md +3 -0
  131. package/gsd-core/workflows/health.md +7 -1
  132. package/gsd-core/workflows/help/modes/full.md +6 -2
  133. package/gsd-core/workflows/import.md +8 -2
  134. package/gsd-core/workflows/inbox.md +7 -0
  135. package/gsd-core/workflows/ingest-docs.md +15 -10
  136. package/gsd-core/workflows/manager.md +3 -1
  137. package/gsd-core/workflows/map-codebase.md +4 -4
  138. package/gsd-core/workflows/mvp-phase.md +3 -0
  139. package/gsd-core/workflows/new-milestone.md +69 -21
  140. package/gsd-core/workflows/new-project.md +17 -15
  141. package/gsd-core/workflows/new-workspace.md +3 -1
  142. package/gsd-core/workflows/onboard.md +3 -0
  143. package/gsd-core/workflows/plan-phase.md +14 -5
  144. package/gsd-core/workflows/plan-review-convergence.md +48 -3
  145. package/gsd-core/workflows/plant-seed.md +3 -0
  146. package/gsd-core/workflows/profile-user.md +7 -1
  147. package/gsd-core/workflows/progress.md +33 -5
  148. package/gsd-core/workflows/quick.md +21 -7
  149. package/gsd-core/workflows/remove-workspace.md +3 -0
  150. package/gsd-core/workflows/review.md +123 -68
  151. package/gsd-core/workflows/scan.md +1 -1
  152. package/gsd-core/workflows/secure-phase.md +4 -1
  153. package/gsd-core/workflows/settings-integrations.md +3 -0
  154. package/gsd-core/workflows/settings.md +3 -0
  155. package/gsd-core/workflows/ship.md +58 -5
  156. package/gsd-core/workflows/sketch.md +3 -0
  157. package/gsd-core/workflows/smart-entry.md +3 -0
  158. package/gsd-core/workflows/spec-phase.md +1 -1
  159. package/gsd-core/workflows/spike.md +7 -1
  160. package/gsd-core/workflows/transition.md +1 -1
  161. package/gsd-core/workflows/ui-phase.md +3 -1
  162. package/gsd-core/workflows/ui-review.md +3 -0
  163. package/gsd-core/workflows/undo.md +7 -0
  164. package/gsd-core/workflows/update.md +2 -0
  165. package/gsd-core/workflows/validate-phase.md +3 -0
  166. package/gsd-core/workflows/verify-phase.md +2 -2
  167. package/gsd-core/workflows/verify-work.md +7 -3
  168. package/hooks/dist/gsd-context-monitor.js +27 -9
  169. package/hooks/dist/gsd-statusline.js +252 -17
  170. package/hooks/gsd-context-monitor.js +27 -9
  171. package/hooks/gsd-statusline.js +252 -17
  172. package/package.json +8 -4
  173. package/pi/gsd.cjs +8 -2
  174. package/scripts/changeset/lint.cjs +1 -0
  175. package/scripts/changeset/parse.cjs +26 -0
  176. package/scripts/check-glossary-refs.cjs +220 -0
  177. package/scripts/ci-rebase-check.cjs +48 -4
  178. package/scripts/ci-test-scope.cjs +39 -1
  179. package/scripts/gen-adr-index.cjs +526 -0
  180. package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
  181. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  182. package/scripts/gen-test-timings.cjs +201 -0
  183. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  184. package/scripts/lint-portable-timeout.cjs +140 -0
  185. package/scripts/lint-table-schema-drift.cjs +157 -0
  186. package/scripts/lint-test-file-count.allowlist.json +1 -0
  187. package/scripts/release-tarball-smoke.cjs +18 -11
  188. package/scripts/run-tests.cjs +420 -58
  189. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  190. package/skills/gsd-mempalace-capture/SKILL.md +31 -1
  191. package/skills/gsd-new-milestone/SKILL.md +1 -1
  192. package/skills/gsd-plan-phase/SKILL.md +5 -3
  193. package/skills/gsd-plan-review-convergence/SKILL.md +3 -2
  194. package/skills/gsd-surface/SKILL.md +6 -6
  195. package/vscode/package.json +1 -1
@@ -6,8 +6,12 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
+ // Namespace (not destructured) so tests can inject spawn failures by
10
+ // monkeypatching childProcess.execFileSync.
11
+ const childProcess = require('child_process');
9
12
  const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
10
13
  const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
14
+ const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
11
15
 
12
16
  // --- Config + last-command readers ------------------------------------------
13
17
 
@@ -286,6 +290,201 @@ function formatGsdState(s) {
286
290
  return parts.join(' · ');
287
291
  }
288
292
 
293
+ // --- Context token count (opt-in) ---------------------------------------------
294
+
295
+ /**
296
+ * Format a token count compactly: 156342 → '156k', 1234567 → '1.2M'.
297
+ */
298
+ function formatTokens(tokens) {
299
+ // Promote to the M branch when k-rounding would reach 1000 (999,500-999,999
300
+ // must render "1.0M", never "1000k").
301
+ if (tokens >= 1000000 || Math.round(tokens / 1000) >= 1000) {
302
+ return (tokens / 1000000).toFixed(1) + 'M';
303
+ }
304
+ if (tokens >= 1000) return Math.round(tokens / 1000) + 'k';
305
+ return String(tokens);
306
+ }
307
+
308
+ /**
309
+ * Pure function: build the token-count suffix for the context meter from the
310
+ * hook input's context_window.current_usage block. Sums input, cache-creation,
311
+ * cache-read, and output tokens (the same total Claude Code's /context shows).
312
+ * Returns ' (156k)' or '' when usage is absent/empty.
313
+ */
314
+ function contextTokenSuffix(currentUsage) {
315
+ if (!currentUsage || typeof currentUsage !== 'object') return '';
316
+ const total = (Number(currentUsage.input_tokens) || 0) +
317
+ (Number(currentUsage.cache_creation_input_tokens) || 0) +
318
+ (Number(currentUsage.cache_read_input_tokens) || 0) +
319
+ (Number(currentUsage.output_tokens) || 0);
320
+ return total > 0 ? ` (${formatTokens(total)})` : '';
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
+
395
+ // --- Model name --------------------------------------------------------------
396
+
397
+ /**
398
+ * Collapse the verbose " (… context)" model-name suffix Claude Code sends for
399
+ * long-context sessions (e.g. "Sonnet 4.5 (1M context)") to a compact badge
400
+ * (" (1M)"). The signal is preserved; the width isn't. Tolerant by design
401
+ * (issue #2160 approval condition): any trailing parenthesized token ending
402
+ * in "context" is collapsed — a future "(500K context)" becomes "(500K)"
403
+ * rather than silently no-opping. The token's own casing is preserved.
404
+ * Any other display name passes through unchanged.
405
+ */
406
+ function compactModelName(name) {
407
+ if (typeof name !== 'string') return name;
408
+ return name.replace(/\s*\(([^)]+?)\s+(?:context|ctx)\)$/i, ' ($1)');
409
+ }
410
+
411
+ // --- Git segment (opt-in) ------------------------------------------------------
412
+ //
413
+ // Opt-in via `statusline.show_git: true` in .planning/config.json. Renders the
414
+ // current branch plus compact work-state markers after the directory segment:
415
+ // " │ main+2~1?3↑1" (staged / unstaged / untracked / ahead / behind)
416
+ // " │ main✓" (clean, in sync)
417
+ // One `git status --porcelain=v2 --branch` spawn per render — no shell, args
418
+ // are a fixed array, and the workspace dir is passed via -C. Fails silently
419
+ // (segment absent) outside a repo, without git, or on timeout.
420
+
421
+ const GIT_STATUS_TIMEOUT_MS = 1500;
422
+
423
+ /**
424
+ * Run `git status --porcelain=v2 --branch` in dir.
425
+ * Returns raw stdout, or null when git is missing, dir isn't a repo, or the
426
+ * call times out. Never throws.
427
+ */
428
+ function readGitStatus(dir) {
429
+ try {
430
+ // 8 MiB maxBuffer (default 1 MiB) headroom for repos with very many changed
431
+ // or untracked files; overflow still degrades safely to segment-absent via
432
+ // the catch below.
433
+ return childProcess.execFileSync('git', ['-C', dir, 'status', '--porcelain=v2', '--branch'],
434
+ { encoding: 'utf8', timeout: GIT_STATUS_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
435
+ } catch (e) {
436
+ return null;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Pure function: parse `git status --porcelain=v2 --branch` output.
442
+ *
443
+ * Returns { branch, ahead, behind, staged, unstaged, untracked } or null when
444
+ * the text carries no branch header (not a repo / unparseable). Detached HEAD
445
+ * reports branch "(detached)" — porcelain v2's literal spelling, shown as-is.
446
+ * Unmerged (conflict) entries count as unstaged: they're pending work either way.
447
+ */
448
+ function parseGitStatus(text) {
449
+ if (typeof text !== 'string') return null;
450
+ const info = { branch: null, ahead: 0, behind: 0, staged: 0, unstaged: 0, untracked: 0 };
451
+ for (const line of text.split('\n')) {
452
+ if (line.startsWith('# branch.head ')) {
453
+ info.branch = line.slice('# branch.head '.length).trim() || null;
454
+ } else if (line.startsWith('# branch.ab ')) {
455
+ const m = line.match(/\+(\d+) -(\d+)/);
456
+ if (m) { info.ahead = parseInt(m[1], 10); info.behind = parseInt(m[2], 10); }
457
+ } else if (line.startsWith('1 ') || line.startsWith('2 ')) {
458
+ // Changed / renamed entries: XY pair at cols 2-3, '.' = unmodified side
459
+ const xy = line.slice(2, 4);
460
+ if (xy[0] !== '.') info.staged++;
461
+ if (xy[1] !== '.') info.unstaged++;
462
+ } else if (line.startsWith('u ')) {
463
+ info.unstaged++;
464
+ } else if (line.startsWith('? ')) {
465
+ info.untracked++;
466
+ }
467
+ }
468
+ return info.branch ? info : null;
469
+ }
470
+
471
+ /**
472
+ * Pure function: format parsed git info into the statusline segment, divider
473
+ * included (mirrors lastCmdSuffix). Branch is dimmed to match the directory
474
+ * segment; markers keep their own colors. Returns '' when info is absent.
475
+ */
476
+ function buildGitSegment(info) {
477
+ if (!info || !info.branch) return '';
478
+ const markers = [];
479
+ if (info.staged) markers.push(`\x1b[32m+${info.staged}\x1b[0m`);
480
+ if (info.unstaged) markers.push(`\x1b[33m~${info.unstaged}\x1b[0m`);
481
+ if (info.untracked) markers.push(`\x1b[31m?${info.untracked}\x1b[0m`);
482
+ if (info.ahead) markers.push(`\x1b[32m↑${info.ahead}\x1b[0m`);
483
+ if (info.behind) markers.push(`\x1b[31m↓${info.behind}\x1b[0m`);
484
+ const state = markers.length ? markers.join('') : '\x1b[32m✓\x1b[0m';
485
+ return ` │ \x1b[2m${info.branch}\x1b[0m${state}`;
486
+ }
487
+
289
488
  // --- stdin ------------------------------------------------------------------
290
489
 
291
490
  function runStatusline() {
@@ -299,11 +498,16 @@ function runStatusline() {
299
498
  clearTimeout(stdinTimeout);
300
499
  try {
301
500
  const data = JSON.parse(input);
302
- const model = data.model?.display_name || 'Claude';
501
+ const model = compactModelName(data.model?.display_name || 'Claude');
303
502
  const dir = data.workspace?.current_dir || process.cwd();
304
503
  const session = data.session_id || '';
305
504
  const remaining = data.context_window?.remaining_percentage;
306
505
 
506
+ // Read .planning config once — used by the context meter (token suffix)
507
+ // and the last-command/position block below. Fail-soft to {}.
508
+ let cfg = {};
509
+ try { cfg = readGsdConfig(dir); } catch (e) {}
510
+
307
511
  // Context window display (shows USED percentage scaled to usable context)
308
512
  // Claude Code reserves a buffer for autocompact. By default this is ~16.5%
309
513
  // of the total window, but users can override it via CLAUDE_CODE_AUTO_COMPACT_WINDOW
@@ -349,15 +553,21 @@ function runStatusline() {
349
553
  const filled = Math.floor(used / 10);
350
554
  const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
351
555
 
556
+ // Opt-in absolute token count after the percentage (statusline.show_context_tokens)
557
+ let tokenSuffix = '';
558
+ if (getConfigValue(cfg, 'statusline.show_context_tokens') === true) {
559
+ tokenSuffix = contextTokenSuffix(data.context_window?.current_usage);
560
+ }
561
+
352
562
  // Color based on usable context thresholds
353
563
  if (used < 50) {
354
- ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`;
564
+ ctx = ` \x1b[32m${bar} ${used}%${tokenSuffix}\x1b[0m`;
355
565
  } else if (used < 65) {
356
- ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`;
566
+ ctx = ` \x1b[33m${bar} ${used}%${tokenSuffix}\x1b[0m`;
357
567
  } else if (used < 80) {
358
- ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`;
568
+ ctx = ` \x1b[38;5;208m${bar} ${used}%${tokenSuffix}\x1b[0m`;
359
569
  } else {
360
- ctx = ` \x1b[5;31m💀 ${bar} ${used}%\x1b[0m`;
570
+ ctx = ` \x1b[5;31m💀 ${bar} ${used}%${tokenSuffix}\x1b[0m`;
361
571
  }
362
572
  }
363
573
 
@@ -392,8 +602,9 @@ function runStatusline() {
392
602
  }
393
603
  }
394
604
 
395
- // GSD state (milestone · status · phase) — shown when no todo task
396
- 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 = '';
397
608
 
398
609
  // GSD update available?
399
610
  // Read only the per-package shared cache file (#607). The legacy
@@ -421,8 +632,9 @@ function runStatusline() {
421
632
  // Failure here must never break the statusline — wrap the entire lookup.
422
633
  let lastCmdSuffix = '';
423
634
  let position = 'end';
635
+ let stateFormat = 'full';
636
+ let gitSuffix = '';
424
637
  try {
425
- const cfg = readGsdConfig(dir);
426
638
  if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
427
639
  const transcriptPath = data.transcript_path;
428
640
  const lastCmd = readLastSlashCommand(transcriptPath);
@@ -432,8 +644,17 @@ function runStatusline() {
432
644
  }
433
645
  const cfgPos = getConfigValue(cfg, 'statusline.context_position');
434
646
  if (cfgPos != null) position = cfgPos;
647
+ if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
648
+ if (getConfigValue(cfg, 'statusline.show_git') === true) {
649
+ gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
650
+ }
435
651
  } catch (e) {
436
- // Never break the statusline on config/transcript errors
652
+ // Never break the statusline on config/transcript/git errors
653
+ }
654
+
655
+ if (!task) {
656
+ const state = readGsdState(dir) || {};
657
+ gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
437
658
  }
438
659
 
439
660
  // Output
@@ -444,7 +665,7 @@ function runStatusline() {
444
665
  ? `\x1b[2m${gsdStateStr}\x1b[0m`
445
666
  : null;
446
667
 
447
- process.stdout.write(composeStatusline({ gsdUpdate, model, ctx, middle, dirname, lastCmdSuffix, position }));
668
+ process.stdout.write(composeStatusline({ gsdUpdate, model, ctx, middle, dirname, lastCmdSuffix, gitSuffix, position }));
448
669
  } catch (e) {
449
670
  // Silent fail - don't break statusline on parse errors
450
671
  }
@@ -463,6 +684,7 @@ function runStatusline() {
463
684
  * @param {string|null} [opts.middle=null] - middle segment (todo task or GSD state), null = absent
464
685
  * @param {string} opts.dirname - project directory basename (dim styling applied here)
465
686
  * @param {string} [opts.lastCmdSuffix=''] - last-command suffix, e.g. ' │ last: /foo'
687
+ * @param {string} [opts.gitSuffix=''] - git branch/status segment, e.g. ' │ main✓' (after dirname)
466
688
  * @param {'end'|'front'} [opts.position='end']
467
689
  * - 'end' (default): ctx appended after dirname — preserved byte-for-byte
468
690
  * - 'front': ctx immediately after model name so the meter stays visible in narrow terminals
@@ -478,6 +700,7 @@ function composeStatusline({
478
700
  middle = null,
479
701
  dirname,
480
702
  lastCmdSuffix = '',
703
+ gitSuffix = '',
481
704
  position = 'end',
482
705
  } = {}) {
483
706
  const modelSeg = `\x1b[2m${model}\x1b[0m`;
@@ -486,12 +709,12 @@ function composeStatusline({
486
709
  const pos = position === 'front' ? 'front' : 'end';
487
710
 
488
711
  if (pos === 'front') {
489
- if (middle) return `${gsdUpdate}${modelSeg}${ctx} │ ${middle} │ ${dirSeg}${lastCmdSuffix}`;
490
- return `${gsdUpdate}${modelSeg}${ctx} │ ${dirSeg}${lastCmdSuffix}`;
712
+ if (middle) return `${gsdUpdate}${modelSeg}${ctx} │ ${middle} │ ${dirSeg}${gitSuffix}${lastCmdSuffix}`;
713
+ return `${gsdUpdate}${modelSeg}${ctx} │ ${dirSeg}${gitSuffix}${lastCmdSuffix}`;
491
714
  }
492
715
  // 'end' — preserved byte-for-byte relative to original inline templates
493
- if (middle) return `${gsdUpdate}${modelSeg} │ ${middle} │ ${dirSeg}${ctx}${lastCmdSuffix}`;
494
- return `${gsdUpdate}${modelSeg} │ ${dirSeg}${ctx}${lastCmdSuffix}`;
716
+ if (middle) return `${gsdUpdate}${modelSeg} │ ${middle} │ ${dirSeg}${gitSuffix}${ctx}${lastCmdSuffix}`;
717
+ return `${gsdUpdate}${modelSeg} │ ${dirSeg}${gitSuffix}${ctx}${lastCmdSuffix}`;
495
718
  }
496
719
 
497
720
  function isInstalledAheadOfLatest(installed, latest) {
@@ -531,6 +754,11 @@ module.exports = {
531
754
  composeStatusline,
532
755
  isInstalledAheadOfLatest,
533
756
  evaluateUpdateCache,
757
+ formatTokens,
758
+ contextTokenSuffix,
759
+ shortGsdStatus, formatGsdStateCompact,
760
+ compactModelName,
761
+ readGitStatus, parseGitStatus, buildGitSegment,
534
762
  };
535
763
 
536
764
  /**
@@ -538,12 +766,14 @@ module.exports = {
538
766
  * testing without feeding stdin. Returns the rendered string.
539
767
  */
540
768
  function renderStatusline(data) {
541
- const model = data.model?.display_name || 'Claude';
769
+ const model = compactModelName(data.model?.display_name || 'Claude');
542
770
  const dir = data.workspace?.current_dir || process.cwd();
543
771
  const dirname = path.basename(dir);
544
772
 
545
773
  let lastCmdSuffix = '';
546
774
  let position = 'end';
775
+ let stateFormat = 'full';
776
+ let gitSuffix = '';
547
777
  try {
548
778
  const cfg = readGsdConfig(dir);
549
779
  if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
@@ -554,11 +784,16 @@ function renderStatusline(data) {
554
784
  }
555
785
  const cfgPos = getConfigValue(cfg, 'statusline.context_position');
556
786
  if (cfgPos != null) position = cfgPos;
787
+ if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
788
+ if (getConfigValue(cfg, 'statusline.show_git') === true) {
789
+ gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
790
+ }
557
791
  } catch (e) { /* swallow */ }
558
792
 
559
- const gsdStateStr = formatGsdState(readGsdState(dir) || {});
793
+ const state = readGsdState(dir) || {};
794
+ const gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
560
795
  const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
561
- return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, position });
796
+ return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position });
562
797
  }
563
798
 
564
799
  module.exports.renderStatusline = renderStatusline;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.7.0-rc.6",
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"
@@ -90,6 +92,7 @@
90
92
  "gen:plugin-skills": "node scripts/gen-plugin-skills.cjs --write",
91
93
  "gen:capability-registry": "node scripts/gen-capability-registry.cjs --write",
92
94
  "gen:registry": "node scripts/gen-registry.cjs --write",
95
+ "gen:golden": "node scripts/gen-golden-install-parity-zcode.cjs && node scripts/gen-install-tree-fixtures.cjs",
93
96
  "validate:registry": "node scripts/validate-registry.cjs",
94
97
  "prepack": "npm run build:lib",
95
98
  "prepare": "npm run build:lib",
@@ -99,7 +102,8 @@
99
102
  "pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
100
103
  "lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
101
104
  "lint:fix": "eslint . --fix",
102
- "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",
105
+ "lint:table-schema-drift": "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",
103
107
  "lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
104
108
  "lint:regression-names": "node scripts/lint-regression-test-names.cjs",
105
109
  "lint:descriptions": "node scripts/lint-descriptions.cjs",
@@ -107,7 +111,7 @@
107
111
  "lint:test-file-count": "node scripts/lint-test-file-count.cjs",
108
112
  "lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
109
113
  "lint:changeset": "node scripts/changeset/lint.cjs",
110
- "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",
111
115
  "lint:docs": "node scripts/lint-docs-required.cjs",
112
116
  "lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
113
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