@opengsd/gsd-core 1.7.0-rc.2 → 1.7.0-rc.4

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 (184) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/GEMINI.md +25 -23
  4. package/README.ja-JP.md +4 -3
  5. package/README.ko-KR.md +4 -3
  6. package/README.md +7 -6
  7. package/README.pt-BR.md +4 -3
  8. package/README.zh-CN.md +4 -3
  9. package/agents/gsd-advisor-researcher.md +1 -1
  10. package/agents/gsd-ai-researcher.md +1 -1
  11. package/agents/gsd-code-fixer.md +1 -1
  12. package/agents/gsd-debug-session-manager.md +1 -1
  13. package/agents/gsd-debugger.md +1 -1
  14. package/agents/gsd-domain-researcher.md +1 -1
  15. package/agents/gsd-eval-auditor.md +1 -1
  16. package/agents/gsd-executor.md +4 -4
  17. package/agents/gsd-intel-updater.md +1 -1
  18. package/agents/gsd-mempalace-curator.md +1 -1
  19. package/agents/gsd-phase-researcher.md +2 -2
  20. package/agents/gsd-plan-checker.md +1 -1
  21. package/agents/gsd-planner.md +2 -2
  22. package/agents/gsd-project-researcher.md +2 -2
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-researcher.md +2 -2
  25. package/agents/gsd-verifier.md +2 -2
  26. package/bin/install.js +132 -464
  27. package/commands/gsd/map-codebase.md +4 -4
  28. package/commands/gsd/mempalace-capture.md +1 -1
  29. package/commands/gsd/mempalace-recall.md +9 -5
  30. package/commands/gsd/next.md +29 -0
  31. package/commands/gsd/ns-project.md +2 -1
  32. package/commands/gsd/ns-workflow.md +5 -2
  33. package/commands/gsd/onboard.md +46 -0
  34. package/gsd-core/bin/ensure-runtime-build.cjs +246 -0
  35. package/gsd-core/bin/gsd-tools.cjs +72 -3
  36. package/gsd-core/bin/lib/capability-registry.cjs +684 -264
  37. package/gsd-core/bin/lib/capability-state.cjs +117 -18
  38. package/gsd-core/bin/lib/capability-writer.cjs +11 -1
  39. package/gsd-core/bin/lib/check-command-router.cjs +111 -1
  40. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +136 -0
  41. package/gsd-core/bin/lib/claude-orchestration.cjs +404 -0
  42. package/gsd-core/bin/lib/clusters.cjs +2 -0
  43. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  44. package/gsd-core/bin/lib/config.cjs +19 -1
  45. package/gsd-core/bin/lib/core-utils.cjs +41 -0
  46. package/gsd-core/bin/lib/external-job.cjs +287 -0
  47. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +151 -0
  48. package/gsd-core/bin/lib/graphify.cjs +41 -9
  49. package/gsd-core/bin/lib/init-command-router.cjs +4 -0
  50. package/gsd-core/bin/lib/init.cjs +25 -69
  51. package/gsd-core/bin/lib/install-engine.cjs +8 -5
  52. package/gsd-core/bin/lib/install-profiles.cjs +2 -0
  53. package/gsd-core/bin/lib/loop-resolver.cjs +7 -1
  54. package/gsd-core/bin/lib/milestone.cjs +7 -1
  55. package/gsd-core/bin/lib/model-resolver.cjs +69 -4
  56. package/gsd-core/bin/lib/onboard-projection.cjs +309 -0
  57. package/gsd-core/bin/lib/phase.cjs +14 -2
  58. package/gsd-core/bin/lib/plan-scan.cjs +9 -1
  59. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  60. package/gsd-core/bin/lib/prohibition-enforcement.cjs +21 -19
  61. package/gsd-core/bin/lib/roadmap.cjs +17 -2
  62. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +18 -13
  63. package/gsd-core/bin/lib/runtime-name-policy.cjs +11 -11
  64. package/gsd-core/bin/lib/shell-command-projection.cjs +12 -8
  65. package/gsd-core/bin/lib/smart-entry.cjs +521 -0
  66. package/gsd-core/bin/lib/surface.cjs +63 -33
  67. package/gsd-core/bin/lib/update-context.cjs +0 -5
  68. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  69. package/gsd-core/bin/shared/model-catalog.json +5 -5
  70. package/gsd-core/bin/shared/runtime-aliases.manifest.json +0 -5
  71. package/gsd-core/references/gates.md +1 -1
  72. package/gsd-core/references/gsd-run-resolver.md +8 -0
  73. package/gsd-core/references/planning-config.md +1 -1
  74. package/gsd-core/templates/project.md +1 -1
  75. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  76. package/gsd-core/workflows/add-backlog.md +1 -1
  77. package/gsd-core/workflows/add-phase.md +1 -1
  78. package/gsd-core/workflows/add-tests.md +1 -1
  79. package/gsd-core/workflows/add-todo.md +1 -1
  80. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  81. package/gsd-core/workflows/audit-fix.md +1 -1
  82. package/gsd-core/workflows/audit-milestone.md +1 -1
  83. package/gsd-core/workflows/audit-uat.md +1 -1
  84. package/gsd-core/workflows/autonomous.md +1 -1
  85. package/gsd-core/workflows/check-todos.md +1 -1
  86. package/gsd-core/workflows/cleanup.md +1 -1
  87. package/gsd-core/workflows/code-review-fix.md +1 -1
  88. package/gsd-core/workflows/code-review.md +1 -1
  89. package/gsd-core/workflows/complete-milestone.md +1 -1
  90. package/gsd-core/workflows/debug.md +1 -1
  91. package/gsd-core/workflows/diagnose-issues.md +1 -1
  92. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  93. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  94. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  95. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  96. package/gsd-core/workflows/discuss-phase.md +2 -2
  97. package/gsd-core/workflows/do.md +5 -4
  98. package/gsd-core/workflows/docs-update.md +1 -1
  99. package/gsd-core/workflows/edit-phase.md +1 -1
  100. package/gsd-core/workflows/eval-review.md +1 -1
  101. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  102. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  103. package/gsd-core/workflows/execute-phase.md +4 -5
  104. package/gsd-core/workflows/execute-plan.md +1 -1
  105. package/gsd-core/workflows/explore.md +1 -1
  106. package/gsd-core/workflows/extract-learnings.md +1 -1
  107. package/gsd-core/workflows/forensics.md +1 -1
  108. package/gsd-core/workflows/graduation.md +1 -1
  109. package/gsd-core/workflows/health.md +1 -1
  110. package/gsd-core/workflows/help/modes/brief.md +2 -1
  111. package/gsd-core/workflows/help/modes/default.md +2 -1
  112. package/gsd-core/workflows/help/modes/full.md +25 -1
  113. package/gsd-core/workflows/help/modes/topic.md +2 -1
  114. package/gsd-core/workflows/import.md +1 -1
  115. package/gsd-core/workflows/ingest-docs.md +1 -1
  116. package/gsd-core/workflows/insert-phase.md +1 -1
  117. package/gsd-core/workflows/list-seeds.md +1 -1
  118. package/gsd-core/workflows/list-workspaces.md +1 -1
  119. package/gsd-core/workflows/manager.md +1 -1
  120. package/gsd-core/workflows/map-codebase.md +1 -1
  121. package/gsd-core/workflows/milestone-summary.md +1 -1
  122. package/gsd-core/workflows/mvp-phase.md +1 -1
  123. package/gsd-core/workflows/new-milestone.md +1 -1
  124. package/gsd-core/workflows/new-project.md +1 -1
  125. package/gsd-core/workflows/new-workspace.md +1 -1
  126. package/gsd-core/workflows/next.md +1 -1
  127. package/gsd-core/workflows/onboard.md +277 -0
  128. package/gsd-core/workflows/pause-work.md +1 -1
  129. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  130. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  131. package/gsd-core/workflows/plan-phase.md +9 -2
  132. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  133. package/gsd-core/workflows/plant-seed.md +1 -1
  134. package/gsd-core/workflows/pr-branch.md +1 -1
  135. package/gsd-core/workflows/profile-user.md +1 -1
  136. package/gsd-core/workflows/progress.md +1 -1
  137. package/gsd-core/workflows/quick.md +21 -1
  138. package/gsd-core/workflows/reapply-patches.md +1 -1
  139. package/gsd-core/workflows/remove-phase.md +1 -1
  140. package/gsd-core/workflows/remove-workspace.md +1 -1
  141. package/gsd-core/workflows/resume-project.md +1 -1
  142. package/gsd-core/workflows/review.md +43 -7
  143. package/gsd-core/workflows/scan.md +1 -1
  144. package/gsd-core/workflows/secure-phase.md +1 -1
  145. package/gsd-core/workflows/settings-advanced.md +3 -1
  146. package/gsd-core/workflows/settings-integrations.md +1 -1
  147. package/gsd-core/workflows/settings.md +1 -1
  148. package/gsd-core/workflows/ship.md +1 -1
  149. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  150. package/gsd-core/workflows/sketch.md +1 -1
  151. package/gsd-core/workflows/smart-entry.md +120 -0
  152. package/gsd-core/workflows/spec-phase.md +8 -6
  153. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  154. package/gsd-core/workflows/spike.md +1 -1
  155. package/gsd-core/workflows/stats.md +1 -1
  156. package/gsd-core/workflows/thread.md +1 -1
  157. package/gsd-core/workflows/transition.md +1 -1
  158. package/gsd-core/workflows/ui-phase.md +1 -1
  159. package/gsd-core/workflows/ui-review.md +1 -1
  160. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  161. package/gsd-core/workflows/update.md +3 -6
  162. package/gsd-core/workflows/validate-phase.md +1 -1
  163. package/gsd-core/workflows/verify-phase.md +1 -1
  164. package/gsd-core/workflows/verify-work.md +62 -4
  165. package/package.json +3 -5
  166. package/scripts/ci-test-scope.cjs +19 -1
  167. package/scripts/gen-golden-install-parity-zcode.cjs +77 -0
  168. package/scripts/generate-package-identity.cjs +27 -2
  169. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -148
  170. package/scripts/lint-regression-test-names.allowlist.json +1 -273
  171. package/scripts/lint-test-file-count.allowlist.json +7 -91
  172. package/scripts/research-profiles.cjs +6 -6
  173. package/scripts/run-tests.cjs +18 -3
  174. package/scripts/slurm-adapter.cjs +269 -0
  175. package/scripts/sync-manifest-versions.cjs +3 -2
  176. package/scripts/sync-runtime-launcher.cjs +20 -2
  177. package/skills/gsd-map-codebase/SKILL.md +3 -3
  178. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  179. package/skills/gsd-mempalace-recall/SKILL.md +9 -5
  180. package/skills/gsd-next/SKILL.md +30 -0
  181. package/skills/gsd-ns-project/SKILL.md +1 -0
  182. package/skills/gsd-ns-workflow/SKILL.md +4 -1
  183. package/skills/gsd-onboard/SKILL.md +46 -0
  184. package/gemini-extension.json +0 -6
@@ -0,0 +1,521 @@
1
+ "use strict";
2
+ /**
3
+ * Smart Entry — state-aware situation classifier for the /gsd front door.
4
+ *
5
+ * Reads project + workflow state (`.planning/STATE.md`, ROADMAP.md, git) and
6
+ * classifies the user's situation into one of a small set of enumerated values,
7
+ * each carrying a recommended next action and a menu of alternatives. Pure
8
+ * detection → classification; no side effects, no writes.
9
+ *
10
+ * Surfaced as `gsd-tools smart-entry [--json]`. The markdown `/gsd` command +
11
+ * workflow shells out to `smart-entry --json`, renders an AskUserQuestion menu
12
+ * (with a --text numbered-list fallback for non-Claude runtimes), and dispatches
13
+ * the chosen action to an existing slash command. See
14
+ * docs/superpowers/specs/2026-06-27-gsd-smart-entry-design.md and
15
+ * docs/adr/1787-gsd-next-smart-entry.md.
16
+ *
17
+ * Relationship to `/gsd:progress --next`: this classifier is a *menu* front door,
18
+ * not a second router. For in-project forward motion (planning → executing →
19
+ * verify-pending) the recommended action delegates to `/gsd:progress --next`
20
+ * (workflows/next.md) — the single, gated advancement engine (Route 0 resume-
21
+ * incomplete-phase invariant + Gates 1-3). smart-entry never re-derives forward
22
+ * routing itself; a standalone advancement command that bypassed those gates is
23
+ * exactly what got the old flat `/gsd-next` removed (#3054). Its distinct value is
24
+ * the states `--next` cannot reach: pre-project (no-project), remediation (paused,
25
+ * blocked, verify-failed), and lifecycle exits (idle-stranded, complete).
26
+ *
27
+ * Design note: this is the gsd-core analog of gsd-pi's `showSmartEntry` branch
28
+ * tree, redesigned for gsd-core's `.planning/` phase loop. gsd-pi's
29
+ * milestone/slice/task model does not exist here; the situation table is the
30
+ * translation, not a port.
31
+ */
32
+ var __importDefault = (this && this.__importDefault) || function (mod) {
33
+ return (mod && mod.__esModule) ? mod : { "default": mod };
34
+ };
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SITUATIONS = void 0;
37
+ exports.detectSignals = detectSignals;
38
+ exports.classify = classify;
39
+ exports.actionsFor = actionsFor;
40
+ exports.classifyProject = classifyProject;
41
+ exports.runSmartEntry = runSmartEntry;
42
+ const node_fs_1 = __importDefault(require("node:fs"));
43
+ const node_path_1 = __importDefault(require("node:path"));
44
+ const node_child_process_1 = require("node:child_process");
45
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
46
+ const ioMod = require("./io.cjs");
47
+ const { output } = ioMod;
48
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
49
+ const planningWorkspace = require("./planning-workspace.cjs");
50
+ const { planningPaths } = planningWorkspace;
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
52
+ const frontmatter = require("./frontmatter.cjs");
53
+ const { extractFrontmatter } = frontmatter;
54
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
55
+ const stateDocument = require("./state-document.cjs");
56
+ const { stateExtractField } = stateDocument;
57
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
58
+ const phaseId = require("./phase-id.cjs");
59
+ const { comparePhaseNum, extractPhaseToken, normalizePhaseName, phaseTokenMatches } = phaseId;
60
+ // ─── Constants ────────────────────────────────────────────────────────────────
61
+ /**
62
+ * Staleness threshold for idle-stranded detection. A clean tree whose last
63
+ * activity is older than this (with non-complete status) is considered stranded
64
+ * committed-but-unshipped work. Hardcoded for v1; configurable later.
65
+ */
66
+ const IDLE_STALE_MS = 72 * 60 * 60 * 1000; // 72h
67
+ /** @internal a single source of truth so the structure test can assert coverage. */
68
+ exports.SITUATIONS = Object.freeze([
69
+ 'no-project',
70
+ 'paused',
71
+ 'blocked',
72
+ 'verify-failed',
73
+ 'needs-first-phase',
74
+ 'planning',
75
+ 'executing',
76
+ 'verify-pending',
77
+ 'idle-stranded',
78
+ 'complete',
79
+ 'unknown',
80
+ ]);
81
+ // ─── Detection ─────────────────────────────────────────────────────────────────
82
+ /** Frontmatter scalar helper: prefer YAML frontmatter, fall back to body field. */
83
+ function fmScalar(fm, body, key, bodyField) {
84
+ const v = fm[key];
85
+ if (typeof v === 'string' && v.trim())
86
+ return v.trim();
87
+ if (typeof v === 'number' || typeof v === 'boolean')
88
+ return String(v);
89
+ return stateExtractField(body, bodyField);
90
+ }
91
+ /** Read a scalar value from a nested frontmatter object (e.g. progress.total_phases). */
92
+ function fmScalarKey(obj, key) {
93
+ if (!obj || typeof obj !== 'object')
94
+ return null;
95
+ const v = obj[key];
96
+ if (typeof v === 'string' && v.trim())
97
+ return v.trim();
98
+ if (typeof v === 'number' || typeof v === 'boolean')
99
+ return String(v);
100
+ return null;
101
+ }
102
+ function parseIntOrNull(s) {
103
+ if (s === null)
104
+ return null;
105
+ const cleaned = s.replace('%', '').trim();
106
+ const n = Number.parseInt(cleaned, 10);
107
+ return Number.isFinite(n) ? n : null;
108
+ }
109
+ function phaseTokenFromDirName(name) {
110
+ const token = extractPhaseToken(name);
111
+ return /^\d+(?:[A-Z])?(?:\.\d+)*(?:-|$)/i.test(token) ? token : null;
112
+ }
113
+ /**
114
+ * Parse a `last_activity` value that may be an ISO date or a free-form string
115
+ * into an epoch-ms timestamp. Returns null when unparseable.
116
+ */
117
+ function parseActivityTimestamp(raw) {
118
+ if (!raw)
119
+ return null;
120
+ const ms = Date.parse(raw);
121
+ return Number.isNaN(ms) ? null : ms;
122
+ }
123
+ /** Read-only git signals. Any git error is swallowed → "no git signal". */
124
+ function readGitSignals(cwd) {
125
+ const run = (args) => {
126
+ try {
127
+ return (0, node_child_process_1.execFileSync)('git', args, {
128
+ cwd,
129
+ encoding: 'utf-8',
130
+ maxBuffer: 4 * 1024 * 1024,
131
+ windowsHide: true,
132
+ stdio: ['pipe', 'pipe', 'pipe'],
133
+ });
134
+ }
135
+ catch {
136
+ return '';
137
+ }
138
+ };
139
+ // Cheap "is this a git repo" probe that also doubles as the dirty check.
140
+ const status = run(['status', '--porcelain']);
141
+ if (status === '' && !run(['rev-parse', '--is-inside-work-tree']).trim()) {
142
+ return { has_git: false, dirty: false, unpushed: false };
143
+ }
144
+ const dirty = status.trim().length > 0;
145
+ // `git log @{u}..HEAD` lists commits on this branch not on its upstream.
146
+ // Non-empty ⇒ unpushed. Errors (no upstream, detached HEAD) ⇒ not unpushed.
147
+ const unpushed = run(['log', '@{u}..HEAD', '--oneline']).trim().length > 0;
148
+ return { has_git: true, dirty, unpushed };
149
+ }
150
+ /** Leading numeric phase token from a STATE.md scalar or body `Phase:` value. */
151
+ function phaseTokenFromState(raw) {
152
+ if (!raw?.trim())
153
+ return null;
154
+ const match = raw.trim().match(/^(\d+(?:[A-Z])?(?:\.\d+)*)/i);
155
+ return match ? match[1] : null;
156
+ }
157
+ /**
158
+ * Detect whether the current phase's verify report indicates failure. The
159
+ * canonical signal is a phase summary/verify artifact whose STATUS: marker reads
160
+ * blocked, failed, or fail. Scoped to STATE.md's current phase so a leftover or
161
+ * newer phase tree cannot skew routing. Returns true only on a clear positive signal.
162
+ */
163
+ function detectVerifyFailed(cwd, currentPhaseRaw) {
164
+ const phasesDir = node_path_1.default.join(planningPaths(cwd).phases);
165
+ let entries = [];
166
+ try {
167
+ entries = node_fs_1.default.readdirSync(phasesDir)
168
+ .map((name) => ({ name, phaseToken: phaseTokenFromDirName(name) }))
169
+ .filter((entry) => entry.phaseToken !== null)
170
+ .sort((a, b) => comparePhaseNum(a.phaseToken, b.phaseToken) || a.name.localeCompare(b.name))
171
+ .map((entry) => entry.name);
172
+ }
173
+ catch {
174
+ return false;
175
+ }
176
+ if (entries.length === 0)
177
+ return false;
178
+ const phaseToken = phaseTokenFromState(currentPhaseRaw);
179
+ let targetDir;
180
+ if (phaseToken) {
181
+ const normalized = normalizePhaseName(phaseToken);
182
+ targetDir = entries.find((name) => phaseTokenMatches(name, normalized));
183
+ if (!targetDir)
184
+ return false;
185
+ }
186
+ else {
187
+ // No current phase in state — fall back to the highest-numbered phase dir.
188
+ targetDir = entries[entries.length - 1];
189
+ }
190
+ const latestDir = node_path_1.default.join(phasesDir, targetDir);
191
+ let files = [];
192
+ try {
193
+ files = node_fs_1.default.readdirSync(latestDir);
194
+ }
195
+ catch {
196
+ return false;
197
+ }
198
+ const candidates = files.filter((f) => /summary|verif(?:y|ication)|uat/i.test(f));
199
+ for (const name of candidates) {
200
+ let content = '';
201
+ try {
202
+ content = node_fs_1.default.readFileSync(node_path_1.default.join(latestDir, name), 'utf-8');
203
+ }
204
+ catch {
205
+ continue;
206
+ }
207
+ const statusMatch = content.match(/STATUS:\s*([A-Za-z_-]+)/i);
208
+ if (statusMatch && /\b(blocked|fail(ed)?)\b/i.test(statusMatch[1])) {
209
+ return true;
210
+ }
211
+ }
212
+ return false;
213
+ }
214
+ /** Parse `.planning/STATE.md` (frontmatter + body) into the raw signals we need. */
215
+ function readStateFile(statePath) {
216
+ let content;
217
+ try {
218
+ content = node_fs_1.default.readFileSync(statePath, 'utf-8');
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ const fm = extractFrontmatter(content);
224
+ const body = content.replace(/^---[\s\S]*?---\s*/, '');
225
+ return { fm, body };
226
+ }
227
+ /** Collect the full signal set from disk for `cwd`. Exported for unit tests. */
228
+ function detectSignals(cwd, now = Date.now) {
229
+ const paths = planningPaths(cwd);
230
+ const hasPlanning = node_fs_1.default.existsSync(paths.planning);
231
+ const hasRoadmap = node_fs_1.default.existsSync(paths.roadmap);
232
+ const git = readGitSignals(cwd);
233
+ const empty = {
234
+ current_phase: null,
235
+ total_phases: null,
236
+ status: '',
237
+ progress: null,
238
+ has_planning: hasPlanning,
239
+ has_roadmap: hasRoadmap,
240
+ git_dirty: git.dirty,
241
+ git_unpushed: git.unpushed,
242
+ paused: false,
243
+ blockers: [],
244
+ has_git: git.has_git,
245
+ verify_failed: false,
246
+ stale_activity: false,
247
+ };
248
+ if (!hasPlanning)
249
+ return empty;
250
+ const parsed = readStateFile(paths.state);
251
+ if (!parsed)
252
+ return empty;
253
+ const { fm, body } = parsed;
254
+ // STATE.md has two lineages of schemas (see state.cjs parseProsePhaseField):
255
+ // - scalar frontmatter: `current_phase: 3`, `total_phases: 5`
256
+ // - nested frontmatter: `progress: { total_phases: 5, percent: 40 }`
257
+ // - body prose: `Phase: 3`, `**Status:** verifying`, `Total Phases: 5`
258
+ // Read each field across every form, scalar-first then nested then body, so
259
+ // the classifier works on real STATE.md files written by current GSD.
260
+ const statusRaw = fmScalar(fm, body, 'status', 'Status');
261
+ const pausedAtRaw = fmScalar(fm, body, 'paused_at', 'Paused At');
262
+ const lastActivityRaw = fmScalar(fm, body, 'last_activity', 'Last Activity');
263
+ // current_phase: scalar fm → nested (none) → body "Current Phase" → body "Phase".
264
+ // The body `Phase:` field is the canonical location in prose-form STATE.md
265
+ // (e.g. "Phase: 3" or "Phase: 3 — ui-review"); parse the leading number.
266
+ const currentPhaseRaw = fmScalar(fm, body, 'current_phase', 'Current Phase') ??
267
+ stateExtractField(body, 'Phase');
268
+ // total_phases & percent: nested `progress:` object takes precedence in the
269
+ // nested schema; scalar fm / body fields cover the flat schema.
270
+ const progressFm = typeof fm.progress === 'object' ? fm.progress : null;
271
+ const totalPhasesRaw = fmScalarKey(progressFm, 'total_phases') ??
272
+ fmScalar(fm, body, 'total_phases', 'Total Phases');
273
+ const progressRaw = fmScalarKey(progressFm, 'percent') ??
274
+ fmScalar(fm, body, 'progress', 'Progress');
275
+ // Blockers list: `- <text>` items under a `## Blockers` heading.
276
+ const blockers = [];
277
+ const blockersMatch = body.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i); // allow-adhoc-markdown: read-only blockers section-collect in smart-entry.cts; mirrors state.cts (#1372), pending collectSection migration
278
+ if (blockersMatch) {
279
+ const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
280
+ for (const item of items)
281
+ blockers.push(item.replace(/^-\s+/, '').trim());
282
+ }
283
+ const paused = Boolean(pausedAtRaw && pausedAtRaw.trim());
284
+ // Stale = no recorded activity for IDLE_STALE_MS. Used only by idle-stranded.
285
+ // Computed here (with the clock seam) so the pure classify() stays a function
286
+ // of (signals, staleActivity) and detectSignals owns all disk reads.
287
+ const lastActivityMs = parseActivityTimestamp(lastActivityRaw);
288
+ const staleActivity = lastActivityMs !== null && now() - lastActivityMs > IDLE_STALE_MS;
289
+ // Verify-failed may be signalled either by STATE.md status or by a failed
290
+ // STATUS: marker on the current phase's summary/verify artifact.
291
+ const verifyFailed = /\bverify-fail(ed)?|verification-fail|uat-fail\b/i.test(statusRaw || '') ||
292
+ detectVerifyFailed(cwd, currentPhaseRaw);
293
+ return {
294
+ current_phase: parseIntOrNull(currentPhaseRaw),
295
+ total_phases: parseIntOrNull(totalPhasesRaw),
296
+ status: (statusRaw || '').toLowerCase(),
297
+ progress: parseIntOrNull(progressRaw),
298
+ has_planning: hasPlanning,
299
+ has_roadmap: hasRoadmap,
300
+ git_dirty: git.dirty,
301
+ git_unpushed: git.unpushed,
302
+ paused,
303
+ blockers,
304
+ has_git: git.has_git,
305
+ verify_failed: verifyFailed,
306
+ stale_activity: staleActivity,
307
+ };
308
+ }
309
+ // ─── Situation classification ─────────────────────────────────────────────────
310
+ /** True when the workflow has fully completed all phases. */
311
+ function isComplete(s) {
312
+ if (s.total_phases === null || s.current_phase === null)
313
+ return false;
314
+ return s.current_phase >= s.total_phases && /\bcomplete(d)?|done|shipped\b/i.test(s.status);
315
+ }
316
+ /** Idle-stranded: clean tree, committed work not shipped, optionally stale. */
317
+ function isIdleStranded(s) {
318
+ if (/\bcomplete(d)?|done|shipped|paused\b/i.test(s.status))
319
+ return false;
320
+ if (s.git_dirty)
321
+ return false;
322
+ // Unpushed commits are the strongest signal. Otherwise require staleness.
323
+ return s.git_unpushed || s.stale_activity;
324
+ }
325
+ /**
326
+ * Classify signals into a situation. Priority order matters — the first
327
+ * matching predicate wins (e.g. paused beats blocked). Exported for unit tests.
328
+ * Pure function of signals (staleness + verify-failed are precomputed on signals).
329
+ */
330
+ function classify(s) {
331
+ if (!s.has_planning)
332
+ return 'no-project';
333
+ if (s.paused)
334
+ return 'paused';
335
+ if (s.blockers.length > 0)
336
+ return 'blocked';
337
+ if (s.verify_failed)
338
+ return 'verify-failed';
339
+ if (s.total_phases === null || s.total_phases <= 0 || !s.has_roadmap)
340
+ return 'needs-first-phase';
341
+ if (isComplete(s))
342
+ return 'complete';
343
+ if (/\bplanning|planned\b/i.test(s.status))
344
+ return 'planning';
345
+ if (/\bexecut(e|ing)|active|in.progress|building\b/i.test(s.status))
346
+ return 'executing';
347
+ if (/\bverify|review|needs.review|pending.review\b/i.test(s.status))
348
+ return 'verify-pending';
349
+ if (isIdleStranded(s))
350
+ return 'idle-stranded';
351
+ return 'unknown';
352
+ }
353
+ // ─── Action sets ──────────────────────────────────────────────────────────────
354
+ const action = (id, label, command, recommended = false) => ({ id, label, command, recommended });
355
+ function rec(id, label, command) {
356
+ return action(id, label, command, true);
357
+ }
358
+ /** Per-situation ordered action list; recommended action first. */
359
+ function actionsFor(situation, s) {
360
+ const phaseN = s.current_phase ?? '';
361
+ const execLabel = phaseN === '' ? 'Continue executing' : `Continue executing phase ${phaseN}`;
362
+ switch (situation) {
363
+ case 'no-project':
364
+ return [
365
+ rec('new-project', 'Start a new project', '/gsd:new-project'),
366
+ action('map-codebase', 'Map an existing codebase', '/gsd:map-codebase'),
367
+ action('quick', 'Quick task', '/gsd:quick'),
368
+ action('help', 'Show help', '/gsd:help'),
369
+ ];
370
+ case 'paused':
371
+ return [
372
+ rec('resume-work', 'Resume work', '/gsd:resume-work'),
373
+ action('progress', 'Show progress', '/gsd:progress'),
374
+ action('quick', 'Quick task', '/gsd:quick'),
375
+ action('help', 'Show help', '/gsd:help'),
376
+ ];
377
+ case 'blocked':
378
+ return [
379
+ rec('debug', 'Debug the blocker', '/gsd:debug'),
380
+ action('verify-work', 'Verify current work', '/gsd:verify-work'),
381
+ action('capture', 'Capture a note', '/gsd:capture'),
382
+ action('progress', 'Show progress', '/gsd:progress'),
383
+ ];
384
+ case 'verify-failed':
385
+ return [
386
+ rec('verify-work', 'Re-verify work', '/gsd:verify-work'),
387
+ action('debug', 'Debug the failure', '/gsd:debug'),
388
+ action('code-review', 'Review recent work', '/gsd:code-review'),
389
+ action('progress', 'Show progress', '/gsd:progress'),
390
+ ];
391
+ case 'needs-first-phase':
392
+ return [
393
+ rec('discuss-phase', 'Discuss the first phase', '/gsd:discuss-phase'),
394
+ action('plan-phase', 'Plan phase 1', '/gsd:plan-phase'),
395
+ action('quick', 'Quick task', '/gsd:quick'),
396
+ action('progress', 'Show progress', '/gsd:progress'),
397
+ ];
398
+ case 'planning':
399
+ // Forward motion → delegate to the single gated engine (see 'executing').
400
+ return [
401
+ rec('progress-next', `Advance to the next step (plan phase ${phaseN || 1})`, '/gsd:progress --next'),
402
+ action('plan-phase', `Plan phase ${phaseN || 1}`, '/gsd:plan-phase'),
403
+ action('discuss-phase', 'Discuss before planning', '/gsd:discuss-phase'),
404
+ action('progress', 'Show progress', '/gsd:progress'),
405
+ ];
406
+ case 'executing':
407
+ // In-project forward motion delegates to the single gated engine
408
+ // (/gsd:progress --next → workflows/next.md). Its Route 0 resume-incomplete
409
+ // -phase invariant + Gates 1-3 must not be bypassed by dispatching a raw
410
+ // /gsd:execute-phase here — that divergence is why the old flat /gsd-next
411
+ // was removed (#3054). Direct execute stays as an explicit secondary choice.
412
+ return [
413
+ rec('progress-next', 'Advance to the next step', '/gsd:progress --next'),
414
+ action('execute-phase', execLabel, '/gsd:execute-phase'),
415
+ action('quick', 'Quick task', '/gsd:quick'),
416
+ action('code-review', 'Review recent work', '/gsd:code-review'),
417
+ ];
418
+ case 'verify-pending':
419
+ // Forward motion → delegate to the single gated engine (see 'executing').
420
+ return [
421
+ rec('progress-next', 'Advance to the next step (verify)', '/gsd:progress --next'),
422
+ action('verify-work', 'Verify work', '/gsd:verify-work'),
423
+ action('code-review', 'Review recent work', '/gsd:code-review'),
424
+ action('ship', 'Ship completed work', '/gsd:ship'),
425
+ ];
426
+ case 'idle-stranded':
427
+ return [
428
+ rec('ship', 'Ship committed work', '/gsd:ship'),
429
+ action('complete-milestone', 'Complete the milestone', '/gsd:complete-milestone'),
430
+ action('progress', 'Show progress', '/gsd:progress'),
431
+ action('capture', 'Capture a note', '/gsd:capture'),
432
+ ];
433
+ case 'complete':
434
+ return [
435
+ rec('new-milestone', 'Start a new milestone', '/gsd:new-milestone'),
436
+ action('extract-learnings', 'Extract learnings', '/gsd:extract-learnings'),
437
+ action('quick', 'Quick task', '/gsd:quick'),
438
+ action('progress', 'Show progress', '/gsd:progress'),
439
+ ];
440
+ case 'unknown':
441
+ default:
442
+ return [
443
+ rec('progress', 'Show progress', '/gsd:progress'),
444
+ action('progress-next', 'Advance to the next step', '/gsd:progress --next'),
445
+ action('quick', 'Quick task', '/gsd:quick'),
446
+ action('help', 'Show help', '/gsd:help'),
447
+ ];
448
+ }
449
+ }
450
+ // ─── Summary line ─────────────────────────────────────────────────────────────
451
+ function buildSummary(situation, s) {
452
+ switch (situation) {
453
+ case 'no-project':
454
+ return 'No project yet — start fresh or map an existing codebase';
455
+ case 'paused':
456
+ return 'Work is paused — pick up where you left off';
457
+ case 'blocked':
458
+ return `Blocked${s.blockers.length ? ` · ${s.blockers.length} blocker(s)` : ''} — resolve before continuing`;
459
+ case 'verify-failed':
460
+ return 'Recent verification failed — re-verify or debug';
461
+ case 'needs-first-phase':
462
+ return 'Project initialized — plan your first phase';
463
+ case 'planning':
464
+ return `Phase ${s.current_phase ?? '?'} of ${s.total_phases ?? '?'} — needs a plan`;
465
+ case 'executing':
466
+ return progressLine('executing', s);
467
+ case 'verify-pending':
468
+ return progressLine('ready to verify', s);
469
+ case 'idle-stranded':
470
+ return 'Committed work not shipped — time to ship';
471
+ case 'complete':
472
+ return 'All phases complete — start a new milestone';
473
+ case 'unknown':
474
+ default:
475
+ return 'Unsure of state — showing progress';
476
+ }
477
+ }
478
+ function progressLine(tail, s) {
479
+ const parts = [];
480
+ if (s.current_phase !== null && s.total_phases !== null) {
481
+ parts.push(`Phase ${s.current_phase} of ${s.total_phases}`);
482
+ }
483
+ else if (s.current_phase !== null) {
484
+ parts.push(`Phase ${s.current_phase}`);
485
+ }
486
+ if (s.progress !== null)
487
+ parts.push(`${s.progress}%`);
488
+ parts.push(tail);
489
+ return parts.join(' · ');
490
+ }
491
+ // ─── Orchestration ─────────────────────────────────────────────────────────────
492
+ /**
493
+ * Run the full detect → classify → assemble pipeline. Pure: no stdout, no
494
+ * writes. Exported for direct unit testing and reuse. The clock seam drives
495
+ * staleness detection inside detectSignals.
496
+ */
497
+ function classifyProject(cwd, now = Date.now) {
498
+ const signals = detectSignals(cwd, now);
499
+ const situation = classify(signals);
500
+ const actions = actionsFor(situation, signals);
501
+ const recommended = actions.find((a) => a.recommended)?.id ?? 'progress';
502
+ const summary = buildSummary(situation, signals);
503
+ return { situation, recommended, summary, signals, actions };
504
+ }
505
+ /**
506
+ * `gsd-tools smart-entry` entry point. `--json` emits machine JSON; default
507
+ * emits a human-readable summary line. Never throws for a missing `.planning/`
508
+ * (returns `no-project`); other internal failures surface a typed error via
509
+ * `output()` so the workflow can fall back to `/gsd:progress`.
510
+ */
511
+ function runSmartEntry(cwd, args, raw) {
512
+ const json = args.includes('--json');
513
+ const result = classifyProject(cwd);
514
+ if (json) {
515
+ output(result, raw, undefined);
516
+ return;
517
+ }
518
+ // Human mode: a compact one-liner plus the recommended action.
519
+ const human = `${result.summary}\nRecommended: ${result.recommended} → ${result.actions.find((a) => a.id === result.recommended)?.command ?? '/gsd:progress'}`;
520
+ output(null, true, human);
521
+ }