@opengsd/gsd-core 1.3.1 → 1.4.0-rc.2

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 (136) hide show
  1. package/.claude-plugin/plugin.json +23 -0
  2. package/GEMINI.md +53 -0
  3. package/agents/gsd-advisor-researcher.md +1 -20
  4. package/agents/gsd-ai-researcher.md +2 -21
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-code-reviewer.md +1 -1
  7. package/agents/gsd-domain-researcher.md +2 -21
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-eval-planner.md +1 -1
  10. package/agents/gsd-executor.md +1 -1
  11. package/agents/gsd-framework-selector.md +1 -1
  12. package/agents/gsd-nyquist-auditor.md +1 -1
  13. package/agents/gsd-pattern-mapper.md +1 -1
  14. package/agents/gsd-phase-researcher.md +92 -166
  15. package/agents/gsd-planner.md +9 -36
  16. package/agents/gsd-project-researcher.md +62 -141
  17. package/agents/gsd-security-auditor.md +1 -1
  18. package/agents/gsd-ui-auditor.md +1 -1
  19. package/agents/gsd-ui-checker.md +1 -1
  20. package/agents/gsd-ui-researcher.md +3 -22
  21. package/agents/gsd-user-profiler.md +1 -1
  22. package/agents/gsd-verifier.md +8 -2
  23. package/bin/install.js +1977 -339
  24. package/commands/gsd/autonomous.md +2 -0
  25. package/commands/gsd/execute-phase.md +2 -0
  26. package/commands/gsd/graphify.md +11 -6
  27. package/commands/gsd/import.md +6 -2
  28. package/commands/gsd/plan-phase.md +4 -2
  29. package/commands/gsd/progress.md +1 -0
  30. package/commands/gsd/stats.md +1 -0
  31. package/commands/gsd/update.md +3 -2
  32. package/gemini-extension.json +6 -0
  33. package/gsd-core/bin/check-latest-version.cjs +61 -6
  34. package/gsd-core/bin/gsd-tools.cjs +238 -32
  35. package/gsd-core/bin/lib/check-command-router.cjs +1 -0
  36. package/gsd-core/bin/lib/cli-exit.cjs +42 -0
  37. package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
  38. package/gsd-core/bin/lib/commands.cjs +5 -4
  39. package/gsd-core/bin/lib/config.cjs +28 -4
  40. package/gsd-core/bin/lib/core.cjs +72 -28
  41. package/gsd-core/bin/lib/graphify.cjs +2 -2
  42. package/gsd-core/bin/lib/init-command-router.cjs +2 -2
  43. package/gsd-core/bin/lib/init.cjs +19 -3
  44. package/gsd-core/bin/lib/install-profiles.cjs +58 -0
  45. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  46. package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +1 -1
  47. package/gsd-core/bin/lib/intel.cjs +3 -20
  48. package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
  49. package/gsd-core/bin/lib/phase.cjs +3 -3
  50. package/gsd-core/bin/lib/research-provider.cjs +137 -0
  51. package/gsd-core/bin/lib/research-store.cjs +167 -0
  52. package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
  53. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +67 -9
  54. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +56 -0
  55. package/gsd-core/bin/lib/runtime-homes.cjs +32 -11
  56. package/gsd-core/bin/lib/security.cjs +73 -0
  57. package/gsd-core/bin/lib/shell-command-projection.cjs +9 -0
  58. package/gsd-core/bin/lib/surface.cjs +54 -11
  59. package/gsd-core/bin/lib/validate.cjs +2 -2
  60. package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
  61. package/gsd-core/bin/lib/verification.cjs +193 -0
  62. package/gsd-core/bin/lib/verify.cjs +2 -2
  63. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
  64. package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
  65. package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
  66. package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
  67. package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
  68. package/gsd-core/references/planner-load-graph-context.md +36 -0
  69. package/gsd-core/references/planning-config.md +3 -1
  70. package/gsd-core/references/research-documentation-lookup.md +29 -0
  71. package/gsd-core/references/research-philosophy.md +29 -0
  72. package/gsd-core/references/research-verification-protocol.md +27 -0
  73. package/gsd-core/workflows/execute-phase.md +19 -8
  74. package/gsd-core/workflows/help/modes/full.md +4 -3
  75. package/gsd-core/workflows/ingest-docs.md +3 -2
  76. package/gsd-core/workflows/plan-phase.md +14 -10
  77. package/gsd-core/workflows/plan-review-convergence.md +3 -3
  78. package/gsd-core/workflows/review.md +24 -7
  79. package/gsd-core/workflows/ship.md +5 -8
  80. package/gsd-core/workflows/spec-phase.md +2 -1
  81. package/gsd-core/workflows/update.md +34 -6
  82. package/hooks/dist/gsd-config-reload.js +133 -0
  83. package/hooks/dist/gsd-context-monitor.js +1 -1
  84. package/hooks/dist/gsd-cursor-post-tool.js +75 -0
  85. package/hooks/dist/gsd-cursor-session-start.js +52 -0
  86. package/hooks/dist/gsd-workflow-guard.js +1 -0
  87. package/hooks/dist/gsd-worktree-path-guard.js +1 -1
  88. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  89. package/hooks/gsd-config-reload.js +133 -0
  90. package/hooks/gsd-context-monitor.js +1 -1
  91. package/hooks/gsd-cursor-post-tool.js +75 -0
  92. package/hooks/gsd-cursor-session-start.js +52 -0
  93. package/hooks/gsd-workflow-guard.js +1 -0
  94. package/hooks/gsd-worktree-path-guard.js +1 -1
  95. package/hooks/hooks.json +69 -0
  96. package/hooks/managed-hooks-registry.cjs +3 -0
  97. package/package.json +8 -1
  98. package/scripts/affected-tests-lib.cjs +3 -2
  99. package/scripts/build-hooks.js +7 -0
  100. package/scripts/changeset/cli.cjs +226 -28
  101. package/scripts/changeset/lint.cjs +5 -4
  102. package/scripts/changeset/new.cjs +4 -4
  103. package/scripts/check-alias-drift.cjs +77 -71
  104. package/scripts/check-env.cjs +185 -179
  105. package/scripts/check-npm-integrity.cjs +115 -109
  106. package/scripts/ci-guard-runner.cjs +11 -5
  107. package/scripts/ci-prepare-test-scope.cjs +27 -22
  108. package/scripts/ci-rebase-check.cjs +46 -45
  109. package/scripts/ci-test-scope.cjs +126 -22
  110. package/scripts/diff-touches-shipped-paths.cjs +52 -44
  111. package/scripts/gen-inventory-manifest.cjs +38 -32
  112. package/scripts/gen-research-agents.cjs +276 -0
  113. package/scripts/issue-dedupe.cjs +278 -0
  114. package/scripts/lib/cli-exit.cjs +56 -0
  115. package/scripts/lint-command-contract.cjs +28 -22
  116. package/scripts/lint-descriptions.cjs +32 -28
  117. package/scripts/lint-docs-required.cjs +4 -4
  118. package/scripts/lint-legacy-dir-name.cjs +56 -52
  119. package/scripts/lint-pr-check-project-dir.cjs +3 -1
  120. package/scripts/lint-shell-command-projection-drift.cjs +27 -22
  121. package/scripts/lint-skill-deps.cjs +31 -26
  122. package/scripts/lint-test-file-count.allowlist.json +2 -0
  123. package/scripts/lint-test-file-count.cjs +5 -4
  124. package/scripts/mutation-matrix.cjs +6 -3
  125. package/scripts/prompt-injection-scan.sh +1 -1
  126. package/scripts/release-notes/discord-release-summary.cjs +373 -0
  127. package/scripts/release-notes/format-github-release-notes.cjs +8 -3
  128. package/scripts/release-tarball-smoke.cjs +6 -4
  129. package/scripts/research-profiles.cjs +149 -0
  130. package/scripts/run-affected-tests.cjs +2 -1
  131. package/scripts/run-cross-platform-tests.cjs +11 -7
  132. package/scripts/run-tests.cjs +8 -7
  133. package/scripts/strip-prose-atrefs.cjs +1 -1
  134. package/scripts/sync-manifest-versions.cjs +119 -0
  135. package/scripts/sync-runtime-launcher.cjs +0 -3
  136. package/scripts/verify-npm-publish.cjs +14 -26
@@ -26,6 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  };
27
27
  const node_fs_1 = __importDefault(require("node:fs"));
28
28
  const node_path_1 = __importDefault(require("node:path"));
29
+ const node_os_1 = __importDefault(require("node:os"));
29
30
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
30
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
32
  const installProfiles = require("./install-profiles.cjs");
@@ -33,7 +34,7 @@ const { readActiveProfile, resolveProfile, loadSkillsManifest, } = installProfil
33
34
  const clusters_cjs_1 = require("./clusters.cjs");
34
35
  // eslint-disable-next-line @typescript-eslint/no-require-imports
35
36
  const runtimeArtifactLayout = require("./runtime-artifact-layout.cjs");
36
- const { findInstallSourceRoot } = runtimeArtifactLayout;
37
+ const { findInstallSourceRoot, getInstallExports } = runtimeArtifactLayout;
37
38
  const SURFACE_FILE_NAME = '.gsd-surface.json';
38
39
  /**
39
40
  * Read the surface state from a runtime config directory.
@@ -210,8 +211,29 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap) {
210
211
  }
211
212
  const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
212
213
  const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap);
214
+ // Mirror installRuntimeArtifacts: skills kinds get per-runtime path rewrites
215
+ // so SKILL.md bodies reference the install target (pathPrefix), not the
216
+ // converter's default ~/.claude paths (#813). Computed lazily so command-only
217
+ // runtimes do not trigger the install.js require.
218
+ let pathPrefix = null;
213
219
  for (const kind of layout.kinds) {
214
220
  const staged = kind.stage(resolved);
221
+ if (kind.kind === 'skills') {
222
+ const installExports = getInstallExports();
223
+ if (pathPrefix === null) {
224
+ const scope = layout.scope ?? 'global';
225
+ const resolvedTarget = node_path_1.default.resolve(layout.configDir).replace(/\\/g, '/');
226
+ const homeDir = node_os_1.default.homedir().replace(/\\/g, '/');
227
+ pathPrefix = installExports.computePathPrefix({
228
+ isGlobal: scope === 'global',
229
+ isOpencode: layout.runtime === 'opencode',
230
+ isWindowsHost: process.platform === 'win32',
231
+ resolvedTarget,
232
+ homeDir,
233
+ });
234
+ }
235
+ installExports.applyRuntimeContentRewritesInPlace(staged, layout.runtime, pathPrefix);
236
+ }
215
237
  const dest = node_path_1.default.join(layout.configDir, kind.destSubpath);
216
238
  _syncGsdDir(staged, dest, kind, skillManifest);
217
239
  }
@@ -335,20 +357,41 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest) {
335
357
  pruneSkillDirs(destDir, stagedDirs, kindPrefix, manifest);
336
358
  }
337
359
  else {
338
- // commands / agents kind: work with .md files
339
- const stagedFiles = new Set(node_fs_1.default.readdirSync(stagedDir).filter(f => f.endsWith('.md')));
340
- // Copy files from staged to dest (overwrite to keep content current)
360
+ // commands / agents kind: mirror installRuntimeArtifacts (_copyStaged /
361
+ // _removeGsdEntries in bin/install.js) so surface produces the SAME files as a
362
+ // fresh install (#816). Flat command dirs (opencode/cursor/augment/kilo) take
363
+ // the gsd- prefix on copy; namespaced command dirs (commands/gsd) and agents
364
+ // keep their staged names. Copying staged names verbatim previously diverged
365
+ // from install and orphaned the installed gsd-*.md files, and the unscoped
366
+ // prune deleted user-owned command files.
367
+ //
368
+ // NOTE: the destName rule below intentionally mirrors bin/install.js
369
+ // `_copyStaged` (the `namespacedByDir` decision). Keep them in sync.
370
+ const destLast = (typeof kind === 'object' && kind !== null && kind.destSubpath)
371
+ ? node_path_1.default.basename(kind.destSubpath)
372
+ : '';
373
+ const prefixStem = kindPrefix ? kindPrefix.replace(/-$/, '') : '';
374
+ const namespacedByDir = kindName === 'commands' && destLast === prefixStem;
375
+ const stagedFiles = node_fs_1.default.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
376
+ const stagedDestNames = new Set();
341
377
  for (const file of stagedFiles) {
342
- node_fs_1.default.copyFileSync(node_path_1.default.join(stagedDir, file), node_path_1.default.join(destDir, file));
378
+ const destName = (kindName === 'agents' || namespacedByDir)
379
+ ? file
380
+ : `${kindPrefix}${file.slice(0, -3)}.md`;
381
+ node_fs_1.default.copyFileSync(node_path_1.default.join(stagedDir, file), node_path_1.default.join(destDir, destName));
382
+ stagedDestNames.add(destName);
343
383
  }
344
- // Remove gsd-only files from dest that aren't in staged set
345
- // For commands dir: all .md files are gsd skills
346
- // For agents dir: only gsd-* files
347
- const destEntries = node_fs_1.default.readdirSync(destDir).filter(f => f.endsWith('.md'));
348
- for (const file of destEntries) {
384
+ // Prune stale GSD-owned files not in the staged set, preserving user-owned files
385
+ // (mirrors install's prefix-scoped _removeGsdEntries):
386
+ // - agents: only gsd-* are GSD-owned
387
+ // - flat command dirs: only `${kindPrefix}`-prefixed are GSD-owned
388
+ // - namespaced command dirs: the whole dir is GSD-owned
389
+ for (const file of node_fs_1.default.readdirSync(destDir).filter(f => f.endsWith('.md'))) {
349
390
  if (kindName === 'agents' && !file.startsWith('gsd-'))
350
391
  continue;
351
- if (!stagedFiles.has(file)) {
392
+ if (kindName === 'commands' && !namespacedByDir && kindPrefix && !file.startsWith(kindPrefix))
393
+ continue;
394
+ if (!stagedDestNames.has(file)) {
352
395
  try {
353
396
  node_fs_1.default.unlinkSync(node_path_1.default.join(destDir, file));
354
397
  }
@@ -90,7 +90,7 @@ function buildRoadmapPhaseVariants(roadmapContent) {
90
90
  const roadmapPhaseVariants = new Set();
91
91
  // Matches both legacy numeric (Phase 1:), decimal (Phase 2.1:), milestone-prefixed (Phase 2-01:),
92
92
  // and bracket-prefixed (### [GSD] Phase 2-01:) headings.
93
- const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:/gi;
93
+ const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
94
94
  let m;
95
95
  while ((m = phasePattern.exec(roadmapContent)) !== null) {
96
96
  roadmapPhases.add(m[1]);
@@ -102,7 +102,7 @@ function buildRoadmapPhaseVariants(roadmapContent) {
102
102
  function buildNotStartedPhaseVariants(roadmapContent) {
103
103
  const notStartedPhases = new Set();
104
104
  // Also matches milestone-prefixed and bracket-prefixed checklist items.
105
- const uncheckedPattern = /-\s*\[\s\]\s*\*{0,2}Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)[:\s*]/gi;
105
+ const uncheckedPattern = /-\s*\[\s\]\s*\*{0,2}Phase\s+([\w][\w.-]*)[:\s*]/gi;
106
106
  let um;
107
107
  while ((um = uncheckedPattern.exec(roadmapContent)) !== null) {
108
108
  for (const variant of phaseVariants(um[1]))
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /**
3
+ * Verification-status subcommand router.
4
+ * Routes `verification.status <phaseDir>` to verification.cmdVerificationStatus.
5
+ *
6
+ * Note: `verification` (reads verifier-emitted status) is distinct from `verify`
7
+ * (runs verification checks like plan-structure/artifacts). Keep them separate.
8
+ *
9
+ * ADR-457 build-at-publish: source in src/verification-command-router.cts,
10
+ * compiled to gsd-core/bin/lib/verification-command-router.cjs (gitignored).
11
+ */
12
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
13
+ const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
14
+ const { routeCjsCommandFamily } = cjsCommandRouterAdapter;
15
+ // ─── Implementation ───────────────────────────────────────────────────────────
16
+ const VERIFICATION_SUBCOMMANDS = ['status'];
17
+ function routeVerificationCommand({ verification, args, cwd, raw, error, }) {
18
+ routeCjsCommandFamily({
19
+ args,
20
+ subcommands: VERIFICATION_SUBCOMMANDS,
21
+ unsupported: {},
22
+ error,
23
+ unknownMessage: (_subcommand, available) => `Unknown verification subcommand. Available: ${available.join(', ')}`,
24
+ handlers: {
25
+ status: () => verification.cmdVerificationStatus(cwd, args[2], raw),
26
+ },
27
+ });
28
+ }
29
+ module.exports = {
30
+ routeVerificationCommand,
31
+ };
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ /**
3
+ * Verification Status — single queryable home for verification-status routing.
4
+ *
5
+ * Issue #651: consolidate the pass/gaps_found/human_needed routing that was
6
+ * previously scattered across ship.md and execute-phase.md into a single
7
+ * tested module. Both workflow files will later consume this module's routing
8
+ * table as the single source of truth.
9
+ *
10
+ * ADR-457 build-at-publish: source in src/verification.cts, compiled to
11
+ * gsd-core/bin/lib/verification.cjs (gitignored).
12
+ *
13
+ * DEFECT.FRONTMATTER-SCALAR-BROAD-GREP fix: status extraction is scoped to
14
+ * the leading YAML frontmatter block only. A `status:` line in the body (e.g.
15
+ * inside a fenced code block) is ignored — this is the exact failure mode that
16
+ * issue #586 / PR #650 identified. The shared extractFrontmatter parser anchors
17
+ * its regex at byte 0 of the document, which provides this guarantee.
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ const node_fs_1 = __importDefault(require("node:fs"));
23
+ const node_path_1 = __importDefault(require("node:path"));
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- core.cjs is an export= CommonJS module
25
+ const core = require("./core.cjs");
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
27
+ const frontmatterMod = require("./frontmatter.cjs");
28
+ const { output, extractPhaseToken } = core;
29
+ const { extractFrontmatter } = frontmatterMod;
30
+ // ─── Constants ────────────────────────────────────────────────────────────────
31
+ /** The set of status values that the gsd-verifier agent emits. */
32
+ const VERIFIER_STATUSES = ['passed', 'gaps_found', 'human_needed'];
33
+ /**
34
+ * Canonical routing table for verification statuses.
35
+ *
36
+ * This is the single source of truth — ship.md and execute-phase.md will
37
+ * later import from here instead of embedding their own message strings.
38
+ *
39
+ * INTERNAL SENTINELS: 'missing' and 'unknown' are operational states constructed
40
+ * internally — the verifier (gsd-verifier.md) never emits them. The verifier only
41
+ * emits values in VERIFIER_STATUSES (passed|gaps_found|human_needed). The guard in
42
+ * readVerificationStatus excludes 'missing' and 'unknown' from raw-status table
43
+ * lookup so they can only be reached via internal construction paths.
44
+ *
45
+ * For 'gaps_found', next_command is built at call time in readVerificationStatus
46
+ * by substituting the phase number — it is NOT stored as a function in the table.
47
+ */
48
+ const VERIFICATION_ROUTING_TABLE = {
49
+ passed: {
50
+ status: 'passed',
51
+ next_action: 'Verification passed — continue.',
52
+ next_command: '',
53
+ },
54
+ gaps_found: {
55
+ status: 'gaps_found',
56
+ next_action: 'Gaps found. Plan the fixes, then re-run execute-phase before shipping.',
57
+ // next_command is computed at call time; this entry is never returned directly.
58
+ next_command: '',
59
+ },
60
+ human_needed: {
61
+ status: 'human_needed',
62
+ next_action: "Human verification required. Complete the manual tests in the phase's *-UAT.md, then re-run the verify step until status is passed.",
63
+ next_command: '',
64
+ },
65
+ // INTERNAL SENTINEL: constructed when no *-VERIFICATION.md file exists or when
66
+ // the file has no parseable frontmatter status. Never emitted by the verifier.
67
+ missing: {
68
+ status: 'missing',
69
+ next_action: 'No verification report found — the verify step never completed. Re-run execute-phase.',
70
+ next_command: '/gsd:execute-phase',
71
+ },
72
+ // INTERNAL SENTINEL: constructed when the file has a status value not in
73
+ // VERIFIER_STATUSES. Never emitted by the verifier.
74
+ unknown: {
75
+ status: 'unknown',
76
+ next_action: '', // filled in dynamically with the raw value
77
+ next_command: '/gsd:execute-phase',
78
+ },
79
+ };
80
+ /**
81
+ * Build a 'missing' result from the routing table.
82
+ * Used for two early-return paths: no *-VERIFICATION.md file found, and
83
+ * file present but no parseable frontmatter status.
84
+ */
85
+ function missingResult() {
86
+ const route = VERIFICATION_ROUTING_TABLE['missing'];
87
+ return {
88
+ status: route.status,
89
+ next_action: route.next_action,
90
+ next_command: route.next_command,
91
+ };
92
+ }
93
+ /**
94
+ * Read the verification status from the first `*-VERIFICATION.md` file in
95
+ * phaseDir and return the routing result.
96
+ *
97
+ * Behavior:
98
+ * 1. Find the first file matching `*-VERIFICATION.md` (sorted, take first).
99
+ * If none → status 'missing'.
100
+ * 2. Extract `status` from FRONTMATTER ONLY via the shared extractFrontmatter
101
+ * parser (DEFECT.FRONTMATTER-SCALAR-BROAD-GREP fix — parser anchors at byte 0).
102
+ * If no frontmatter block or no `status` key → status 'missing'.
103
+ * 3. Map to routing table. Unknown non-empty value → status 'unknown'.
104
+ *
105
+ * @param phaseDir - Absolute path to the phase directory.
106
+ * @param opts - Options. `opts.fs` allows test injection (defaults to node:fs).
107
+ */
108
+ function readVerificationStatus(phaseDir, opts = {}) {
109
+ const fsImpl = opts.fs ?? node_fs_1.default;
110
+ // Phase token for the gaps_found command
111
+ const baseName = node_path_1.default.basename(phaseDir);
112
+ const phaseToken = extractPhaseToken(baseName);
113
+ const phaseNumber = phaseToken.length > 0 ? phaseToken : baseName;
114
+ // 1. Find *-VERIFICATION.md
115
+ let verificationFile = null;
116
+ try {
117
+ const entries = fsImpl.readdirSync(phaseDir);
118
+ const candidates = entries.filter((f) => f.endsWith('-VERIFICATION.md')).sort();
119
+ verificationFile = candidates.length > 0 ? candidates[0] : null;
120
+ }
121
+ catch {
122
+ // Directory unreadable → treat as missing
123
+ verificationFile = null;
124
+ }
125
+ if (!verificationFile) {
126
+ return missingResult();
127
+ }
128
+ // 2. Read and parse frontmatter using the shared parser.
129
+ // extractFrontmatter anchors at byte 0, so body `status:` lines are ignored.
130
+ const filePath = node_path_1.default.join(phaseDir, verificationFile);
131
+ let rawStatus = null;
132
+ try {
133
+ const content = fsImpl.readFileSync(filePath, 'utf-8');
134
+ const fm = extractFrontmatter(content);
135
+ const statusVal = fm['status'];
136
+ // status is always a scalar string in a well-formed VERIFICATION.md frontmatter;
137
+ // only accept string values — arrays and objects are not valid status values.
138
+ if (typeof statusVal === 'string') {
139
+ const trimmed = statusVal.trim();
140
+ rawStatus = trimmed.length > 0 ? trimmed : null;
141
+ }
142
+ }
143
+ catch {
144
+ rawStatus = null;
145
+ }
146
+ if (!rawStatus) {
147
+ return missingResult();
148
+ }
149
+ // 3. Route — exclude internal sentinels from raw-file lookup (they are
150
+ // constructed internally above, never written by the verifier).
151
+ if (rawStatus in VERIFICATION_ROUTING_TABLE && rawStatus !== 'missing' && rawStatus !== 'unknown') {
152
+ const entry = VERIFICATION_ROUTING_TABLE[rawStatus];
153
+ // gaps_found: build the phase-specific command here rather than in the table.
154
+ const next_command = rawStatus === 'gaps_found'
155
+ ? `/gsd:plan-phase ${phaseNumber} --gaps`
156
+ : entry.next_command;
157
+ return {
158
+ status: entry.status,
159
+ next_action: entry.next_action,
160
+ next_command,
161
+ };
162
+ }
163
+ // Unknown value
164
+ const unknownRoute = VERIFICATION_ROUTING_TABLE['unknown'];
165
+ return {
166
+ status: unknownRoute.status,
167
+ next_action: `Unexpected verification status '${rawStatus}'. Re-run execute-phase verification.`,
168
+ next_command: unknownRoute.next_command,
169
+ };
170
+ }
171
+ /**
172
+ * CLI command handler: resolve phaseDir against cwd, call readVerificationStatus,
173
+ * emit via core.output().
174
+ *
175
+ * @param cwd - Current working directory (used to resolve phaseDirArg).
176
+ * @param phaseDirArg - Phase directory path (absolute or relative to cwd).
177
+ * @param raw - Whether to emit raw (non-JSON) output.
178
+ */
179
+ function cmdVerificationStatus(cwd, phaseDirArg, raw) {
180
+ if (!phaseDirArg) {
181
+ core.error('phase directory required for verification.status');
182
+ return;
183
+ }
184
+ const phaseDir = node_path_1.default.resolve(cwd, phaseDirArg);
185
+ const result = readVerificationStatus(phaseDir);
186
+ output(result, raw);
187
+ }
188
+ module.exports = {
189
+ VERIFIER_STATUSES,
190
+ VERIFICATION_ROUTING_TABLE,
191
+ readVerificationStatus,
192
+ cmdVerificationStatus,
193
+ };
@@ -550,13 +550,13 @@ function cmdValidateConsistency(cwd, raw) {
550
550
  const roadmapContentRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
551
551
  const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
552
552
  const roadmapPhases = new Set();
553
- const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:/gi;
553
+ const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
554
554
  let m;
555
555
  while ((m = phasePattern.exec(roadmapContent)) !== null) {
556
556
  roadmapPhases.add(m[1]);
557
557
  }
558
558
  const fullRoadmapPhases = new Set();
559
- const fullPhasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:/gi;
559
+ const fullPhasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
560
560
  let fm;
561
561
  while ((fm = fullPhasePattern.exec(roadmapContentRaw)) !== null) {
562
562
  fullRoadmapPhases.add(fm[1]);
@@ -20,7 +20,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
20
20
  const node_path_1 = __importDefault(require("node:path"));
21
21
  // eslint-disable-next-line @typescript-eslint/no-require-imports
22
22
  const core = require("./core.cjs");
23
- const { toPosixPath, readSubdirectories } = core;
23
+ const { readSubdirectories } = core;
24
24
  // eslint-disable-next-line @typescript-eslint/no-require-imports
25
25
  const planScan = require("./plan-scan.cjs");
26
26
  // eslint-disable-next-line @typescript-eslint/no-require-imports