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

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 (155) 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.md +3 -3
  5. package/agents/gsd-advisor-researcher.md +1 -1
  6. package/agents/gsd-ai-researcher.md +1 -1
  7. package/agents/gsd-code-fixer.md +1 -1
  8. package/agents/gsd-debug-session-manager.md +1 -1
  9. package/agents/gsd-debugger.md +1 -1
  10. package/agents/gsd-domain-researcher.md +1 -1
  11. package/agents/gsd-eval-auditor.md +1 -1
  12. package/agents/gsd-executor.md +4 -4
  13. package/agents/gsd-intel-updater.md +1 -1
  14. package/agents/gsd-mempalace-curator.md +1 -1
  15. package/agents/gsd-phase-researcher.md +2 -2
  16. package/agents/gsd-plan-checker.md +1 -1
  17. package/agents/gsd-planner.md +2 -2
  18. package/agents/gsd-project-researcher.md +2 -2
  19. package/agents/gsd-research-synthesizer.md +1 -1
  20. package/agents/gsd-ui-researcher.md +2 -2
  21. package/agents/gsd-verifier.md +2 -2
  22. package/bin/install.js +108 -458
  23. package/commands/gsd/mempalace-capture.md +1 -1
  24. package/commands/gsd/mempalace-recall.md +9 -5
  25. package/commands/gsd/next.md +29 -0
  26. package/commands/gsd/ns-workflow.md +5 -2
  27. package/gsd-core/bin/ensure-runtime-build.cjs +246 -0
  28. package/gsd-core/bin/gsd-tools.cjs +25 -1
  29. package/gsd-core/bin/lib/capability-registry.cjs +207 -205
  30. package/gsd-core/bin/lib/check-command-router.cjs +111 -1
  31. package/gsd-core/bin/lib/clusters.cjs +1 -0
  32. package/gsd-core/bin/lib/config.cjs +19 -1
  33. package/gsd-core/bin/lib/core-utils.cjs +41 -0
  34. package/gsd-core/bin/lib/external-job.cjs +287 -0
  35. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +151 -0
  36. package/gsd-core/bin/lib/graphify.cjs +41 -9
  37. package/gsd-core/bin/lib/milestone.cjs +7 -1
  38. package/gsd-core/bin/lib/phase.cjs +14 -2
  39. package/gsd-core/bin/lib/plan-scan.cjs +9 -1
  40. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  41. package/gsd-core/bin/lib/prohibition-enforcement.cjs +21 -19
  42. package/gsd-core/bin/lib/roadmap.cjs +17 -2
  43. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +18 -13
  44. package/gsd-core/bin/lib/runtime-name-policy.cjs +8 -10
  45. package/gsd-core/bin/lib/shell-command-projection.cjs +12 -8
  46. package/gsd-core/bin/lib/smart-entry.cjs +521 -0
  47. package/gsd-core/bin/lib/surface.cjs +18 -9
  48. package/gsd-core/bin/lib/update-context.cjs +0 -5
  49. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  50. package/gsd-core/bin/shared/model-catalog.json +0 -5
  51. package/gsd-core/bin/shared/runtime-aliases.manifest.json +0 -5
  52. package/gsd-core/references/gates.md +1 -1
  53. package/gsd-core/references/planning-config.md +1 -1
  54. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  55. package/gsd-core/workflows/add-backlog.md +1 -1
  56. package/gsd-core/workflows/add-phase.md +1 -1
  57. package/gsd-core/workflows/add-tests.md +1 -1
  58. package/gsd-core/workflows/add-todo.md +1 -1
  59. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  60. package/gsd-core/workflows/audit-fix.md +1 -1
  61. package/gsd-core/workflows/audit-milestone.md +1 -1
  62. package/gsd-core/workflows/audit-uat.md +1 -1
  63. package/gsd-core/workflows/autonomous.md +1 -1
  64. package/gsd-core/workflows/check-todos.md +1 -1
  65. package/gsd-core/workflows/cleanup.md +1 -1
  66. package/gsd-core/workflows/code-review-fix.md +1 -1
  67. package/gsd-core/workflows/code-review.md +1 -1
  68. package/gsd-core/workflows/complete-milestone.md +1 -1
  69. package/gsd-core/workflows/debug.md +1 -1
  70. package/gsd-core/workflows/diagnose-issues.md +1 -1
  71. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  72. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  73. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  74. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  75. package/gsd-core/workflows/discuss-phase.md +2 -2
  76. package/gsd-core/workflows/do.md +1 -1
  77. package/gsd-core/workflows/docs-update.md +1 -1
  78. package/gsd-core/workflows/edit-phase.md +1 -1
  79. package/gsd-core/workflows/eval-review.md +1 -1
  80. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  81. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  82. package/gsd-core/workflows/execute-phase.md +4 -5
  83. package/gsd-core/workflows/execute-plan.md +1 -1
  84. package/gsd-core/workflows/explore.md +1 -1
  85. package/gsd-core/workflows/extract-learnings.md +1 -1
  86. package/gsd-core/workflows/forensics.md +1 -1
  87. package/gsd-core/workflows/graduation.md +1 -1
  88. package/gsd-core/workflows/health.md +1 -1
  89. package/gsd-core/workflows/help/modes/full.md +14 -0
  90. package/gsd-core/workflows/help/modes/topic.md +1 -0
  91. package/gsd-core/workflows/import.md +1 -1
  92. package/gsd-core/workflows/ingest-docs.md +1 -1
  93. package/gsd-core/workflows/insert-phase.md +1 -1
  94. package/gsd-core/workflows/list-seeds.md +1 -1
  95. package/gsd-core/workflows/list-workspaces.md +1 -1
  96. package/gsd-core/workflows/manager.md +1 -1
  97. package/gsd-core/workflows/map-codebase.md +1 -1
  98. package/gsd-core/workflows/milestone-summary.md +1 -1
  99. package/gsd-core/workflows/mvp-phase.md +1 -1
  100. package/gsd-core/workflows/new-milestone.md +1 -1
  101. package/gsd-core/workflows/new-project.md +1 -1
  102. package/gsd-core/workflows/new-workspace.md +1 -1
  103. package/gsd-core/workflows/next.md +1 -1
  104. package/gsd-core/workflows/pause-work.md +1 -1
  105. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  106. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  107. package/gsd-core/workflows/plan-phase.md +9 -2
  108. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  109. package/gsd-core/workflows/plant-seed.md +1 -1
  110. package/gsd-core/workflows/pr-branch.md +1 -1
  111. package/gsd-core/workflows/profile-user.md +1 -1
  112. package/gsd-core/workflows/progress.md +1 -1
  113. package/gsd-core/workflows/quick.md +1 -1
  114. package/gsd-core/workflows/reapply-patches.md +1 -1
  115. package/gsd-core/workflows/remove-phase.md +1 -1
  116. package/gsd-core/workflows/remove-workspace.md +1 -1
  117. package/gsd-core/workflows/resume-project.md +1 -1
  118. package/gsd-core/workflows/review.md +43 -7
  119. package/gsd-core/workflows/scan.md +1 -1
  120. package/gsd-core/workflows/secure-phase.md +1 -1
  121. package/gsd-core/workflows/settings-advanced.md +3 -1
  122. package/gsd-core/workflows/settings-integrations.md +1 -1
  123. package/gsd-core/workflows/settings.md +1 -1
  124. package/gsd-core/workflows/ship.md +1 -1
  125. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  126. package/gsd-core/workflows/sketch.md +1 -1
  127. package/gsd-core/workflows/smart-entry.md +120 -0
  128. package/gsd-core/workflows/spec-phase.md +8 -6
  129. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  130. package/gsd-core/workflows/spike.md +1 -1
  131. package/gsd-core/workflows/stats.md +1 -1
  132. package/gsd-core/workflows/thread.md +1 -1
  133. package/gsd-core/workflows/transition.md +1 -1
  134. package/gsd-core/workflows/ui-phase.md +1 -1
  135. package/gsd-core/workflows/ui-review.md +1 -1
  136. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  137. package/gsd-core/workflows/update.md +3 -6
  138. package/gsd-core/workflows/validate-phase.md +1 -1
  139. package/gsd-core/workflows/verify-phase.md +1 -1
  140. package/gsd-core/workflows/verify-work.md +62 -4
  141. package/package.json +3 -5
  142. package/scripts/ci-test-scope.cjs +19 -1
  143. package/scripts/generate-package-identity.cjs +27 -2
  144. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -148
  145. package/scripts/lint-regression-test-names.allowlist.json +1 -273
  146. package/scripts/lint-test-file-count.allowlist.json +7 -91
  147. package/scripts/research-profiles.cjs +6 -6
  148. package/scripts/run-tests.cjs +16 -3
  149. package/scripts/slurm-adapter.cjs +269 -0
  150. package/scripts/sync-manifest-versions.cjs +3 -2
  151. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  152. package/skills/gsd-mempalace-recall/SKILL.md +9 -5
  153. package/skills/gsd-next/SKILL.md +30 -0
  154. package/skills/gsd-ns-workflow/SKILL.md +4 -1
  155. 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
+ }
@@ -466,16 +466,25 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest) {
466
466
  // - agents: only gsd-* are GSD-owned
467
467
  // - flat command dirs: only `${kindPrefix}`-prefixed are GSD-owned
468
468
  // - namespaced command dirs: the whole dir is GSD-owned
469
- for (const file of node_fs_1.default.readdirSync(destDir).filter(f => f.endsWith('.md'))) {
470
- if (kindName === 'agents' && !file.startsWith('gsd-'))
471
- continue;
472
- if (kindName === 'commands' && !namespacedByDir && kindPrefix && !file.startsWith(kindPrefix))
473
- continue;
474
- if (!stagedDestNames.has(file)) {
475
- try {
476
- node_fs_1.default.unlinkSync(node_path_1.default.join(destDir, file));
469
+ //
470
+ // Manifest gate (#2018): when the manifest is empty/absent (e.g. an unresolvable
471
+ // install source root yields an empty staged dir), the staged set is untrustworthy.
472
+ // Skills are guarded by pruneSkillDirs' manifest-membership check; agents must be
473
+ // guarded here — skip the prune loop entirely so an empty manifest never deletes
474
+ // every gsd-* agent. Copying (above) still runs so genuinely new agents are added.
475
+ const shouldPruneAgents = !(kindName === 'agents' && (!manifest || manifest.size === 0));
476
+ if (shouldPruneAgents) {
477
+ for (const file of node_fs_1.default.readdirSync(destDir).filter(f => f.endsWith('.md'))) {
478
+ if (kindName === 'agents' && !file.startsWith('gsd-'))
479
+ continue;
480
+ if (kindName === 'commands' && !namespacedByDir && kindPrefix && !file.startsWith(kindPrefix))
481
+ continue;
482
+ if (!stagedDestNames.has(file)) {
483
+ try {
484
+ node_fs_1.default.unlinkSync(node_path_1.default.join(destDir, file));
485
+ }
486
+ catch { /* ignore */ }
477
487
  }
478
- catch { /* ignore */ }
479
488
  }
480
489
  }
481
490
  }
@@ -33,7 +33,6 @@ exports.RUNTIME_DIRS = [
33
33
  ['antigravity', '.agent'], // local Antigravity install dir legacy (#503; backward-compat with pre-#791 installs)
34
34
  ['windsurf', '.windsurf'], // local Windsurf workflow dir canonical (#1615; bin/install.js getDirName('windsurf'))
35
35
  ['windsurf', '.devin'], // local Devin Desktop install dir legacy (#1085; backward-compat)
36
- ['gemini', '.gemini'],
37
36
  ['kilo', '.config/kilo'],
38
37
  ['kilo', '.kilo'],
39
38
  ['codex', '.codex'],
@@ -80,8 +79,6 @@ function inferPreferredRuntime({ fs, env, preferredConfigDir }) {
80
79
  return 'codex';
81
80
  if (env['ANTIGRAVITY_CONFIG_DIR'])
82
81
  return 'antigravity';
83
- if (env['GEMINI_CONFIG_DIR'])
84
- return 'gemini';
85
82
  if (env['KILO_CONFIG_DIR'] || env['KILO_CONFIG'])
86
83
  return 'kilo';
87
84
  if (env['OPENCODE_CONFIG_DIR'] || env['OPENCODE_CONFIG'])
@@ -98,8 +95,6 @@ function envRuntimeDirs({ env, home }) {
98
95
  out.push(['claude', ex(env['CLAUDE_CONFIG_DIR'])]);
99
96
  if (env['ANTIGRAVITY_CONFIG_DIR'])
100
97
  out.push(['antigravity', ex(env['ANTIGRAVITY_CONFIG_DIR'])]);
101
- if (env['GEMINI_CONFIG_DIR'])
102
- out.push(['gemini', ex(env['GEMINI_CONFIG_DIR'])]);
103
98
  if (env['KILO_CONFIG_DIR'])
104
99
  out.push(['kilo', ex(env['KILO_CONFIG_DIR'])]);
105
100
  else if (env['KILO_CONFIG'])
@@ -84,6 +84,7 @@
84
84
  "context_window",
85
85
  "graphify.build_timeout",
86
86
  "graphify.auto_update",
87
+ "graphify.graph_path",
87
88
  "claude_md_path",
88
89
  "claude_md_assembly.mode",
89
90
  "runtime",
@@ -17,11 +17,6 @@
17
17
  "sonnet": { "model": "gpt-5.4", "reasoning_effort": "medium" },
18
18
  "haiku": { "model": "gpt-5.4-mini", "reasoning_effort": "medium" }
19
19
  },
20
- "gemini": {
21
- "opus": { "model": "gemini-3.1-pro-preview" },
22
- "sonnet": { "model": "gemini-3-flash" },
23
- "haiku": { "model": "gemini-2.5-flash-lite" }
24
- },
25
20
  "qwen": {
26
21
  "opus": { "model": "qwen3-max-2026-01-23" },
27
22
  "sonnet": { "model": "qwen3-coder-plus" },
@@ -13,11 +13,6 @@
13
13
  "kilo",
14
14
  "kilo-cli"
15
15
  ],
16
- "gemini": [
17
- "gemini",
18
- "gemini-cli",
19
- "gemini-code"
20
- ],
21
16
  "codex": [
22
17
  "codex",
23
18
  "codex-app",
@@ -38,7 +38,7 @@ Canonical gate types used across GSD workflows. Every validation checkpoint maps
38
38
  **Recovery:** Developer investigates root cause, fixes, restarts from checkpoint.
39
39
  **Examples:**
40
40
  - Context window critically low during execution
41
- - STATE.md in error state blocking /gsd-next
41
+ - STATE.md in error state blocking /gsd:next
42
42
  - Verification finds critical missing deliverables
43
43
 
44
44
  ---
@@ -317,7 +317,7 @@ Set via `features.*` namespace (e.g., `"features": { "thinking_partner": true }`
317
317
  | Key | Type | Default | Allowed Values | Description |
318
318
  |-----|------|---------|----------------|-------------|
319
319
  | `features.thinking_partner` | boolean | `false` | `true`, `false` | Enable conditional extended thinking at workflow decision points (used by discuss-phase and plan-phase for architectural tradeoff analysis) |
320
- | `features.global_learnings` | boolean | `false` | `true`, `false` | Enable injection of global learnings from `~/.gsd/learnings/` into agent prompts |
320
+ | `features.global_learnings` | boolean | `false` | `true`, `false` | Enable injection of global learnings from `~/.gsd/knowledge/` into agent prompts |
321
321
 
322
322
  ### Hook Fields
323
323