@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +14 -0
- package/README.md +2 -0
- package/agents/gsd-debug-session-manager.md +42 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +29 -2
- package/agents/gsd-planner.md +29 -36
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1152 -80
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +3 -2
- package/gsd-core/bin/gsd-tools.cjs +1878 -2507
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +338 -45
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-registry.cjs +155 -86
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +128 -25
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
- package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +81 -4
- package/gsd-core/bin/lib/config-loader.cjs +14 -2
- package/gsd-core/bin/lib/config.cjs +69 -18
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/init.cjs +111 -47
- package/gsd-core/bin/lib/install-engine.cjs +298 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +246 -12
- package/gsd-core/bin/lib/model-catalog.cjs +19 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-id.cjs +26 -4
- package/gsd-core/bin/lib/phase.cjs +201 -12
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +7 -4
- package/gsd-core/bin/lib/roadmap.cjs +13 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -8
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +16 -0
- package/gsd-core/bin/lib/smart-entry.cjs +69 -4
- package/gsd-core/bin/lib/state-document.cjs +7 -4
- package/gsd-core/bin/lib/state-transition.cjs +22 -1
- package/gsd-core/bin/lib/state.cjs +65 -11
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat.cjs +420 -5
- package/gsd-core/bin/lib/validate.cjs +12 -8
- package/gsd-core/bin/lib/verification.cjs +112 -17
- package/gsd-core/bin/lib/verify.cjs +220 -22
- package/gsd-core/bin/shared/config-schema.manifest.json +3 -2
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/reviewer-instances.md +9 -7
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -2
- package/gsd-core/workflows/audit-fix.md +2 -2
- package/gsd-core/workflows/check-todos.md +3 -1
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review.md +17 -5
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +25 -5
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +1 -0
- package/gsd-core/workflows/eval-review.md +3 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +25 -34
- package/gsd-core/workflows/execute-plan.md +15 -4
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +6 -2
- package/gsd-core/workflows/import.md +8 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +15 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +69 -21
- package/gsd-core/workflows/new-project.md +17 -15
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +14 -5
- package/gsd-core/workflows/plan-review-convergence.md +48 -3
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +19 -7
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +89 -73
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -0
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +50 -3
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +3 -0
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +3 -1
- package/gsd-core/workflows/ui-review.md +3 -0
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +2 -0
- package/gsd-core/workflows/validate-phase.md +3 -0
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +7 -3
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-statusline.js +88 -3
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-statusline.js +88 -3
- package/package.json +6 -4
- package/pi/gsd.cjs +8 -2
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-glossary-refs.cjs +220 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/gen-adr-index.cjs +526 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +3 -2
- 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
|
-
|
|
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
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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);
|
package/hooks/gsd-statusline.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
13
|
-
*
|
|
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
|
|
@@ -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
|
|