@opengsd/gsd-core 1.7.0-rc.1 → 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 (191) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +711 -0
  4. package/GEMINI.md +25 -23
  5. package/README.md +3 -3
  6. package/agents/gsd-advisor-researcher.md +3 -1
  7. package/agents/gsd-ai-researcher.md +2 -2
  8. package/agents/gsd-assumptions-analyzer.md +2 -0
  9. package/agents/gsd-code-fixer.md +3 -1
  10. package/agents/gsd-code-reviewer.md +2 -0
  11. package/agents/gsd-codebase-mapper.md +2 -0
  12. package/agents/gsd-debug-session-manager.md +1 -1
  13. package/agents/gsd-debugger.md +3 -1
  14. package/agents/gsd-doc-writer.md +2 -0
  15. package/agents/gsd-domain-researcher.md +1 -1
  16. package/agents/gsd-eval-auditor.md +3 -1
  17. package/agents/gsd-executor.md +12 -9
  18. package/agents/gsd-integration-checker.md +2 -0
  19. package/agents/gsd-intel-updater.md +1 -1
  20. package/agents/gsd-mempalace-curator.md +1 -1
  21. package/agents/gsd-nyquist-auditor.md +2 -0
  22. package/agents/gsd-phase-researcher.md +4 -2
  23. package/agents/gsd-plan-checker.md +3 -1
  24. package/agents/gsd-planner.md +4 -2
  25. package/agents/gsd-project-researcher.md +4 -2
  26. package/agents/gsd-research-synthesizer.md +3 -1
  27. package/agents/gsd-roadmapper.md +2 -0
  28. package/agents/gsd-security-auditor.md +2 -0
  29. package/agents/gsd-ui-auditor.md +2 -0
  30. package/agents/gsd-ui-checker.md +2 -0
  31. package/agents/gsd-ui-researcher.md +4 -2
  32. package/agents/gsd-verifier.md +6 -4
  33. package/bin/install.js +226 -459
  34. package/commands/gsd/mempalace-capture.md +1 -1
  35. package/commands/gsd/mempalace-recall.md +9 -5
  36. package/commands/gsd/next.md +29 -0
  37. package/commands/gsd/ns-workflow.md +5 -2
  38. package/gsd-core/bin/ensure-runtime-build.cjs +246 -0
  39. package/gsd-core/bin/gsd-tools.cjs +43 -8
  40. package/gsd-core/bin/lib/capability-loader.cjs +27 -9
  41. package/gsd-core/bin/lib/capability-registry.cjs +209 -205
  42. package/gsd-core/bin/lib/capability-source.cjs +22 -7
  43. package/gsd-core/bin/lib/capability-validator.cjs +24 -2
  44. package/gsd-core/bin/lib/check-command-router.cjs +111 -1
  45. package/gsd-core/bin/lib/clusters.cjs +1 -0
  46. package/gsd-core/bin/lib/commands.cjs +2 -1
  47. package/gsd-core/bin/lib/config.cjs +19 -1
  48. package/gsd-core/bin/lib/core-utils.cjs +41 -0
  49. package/gsd-core/bin/lib/external-job.cjs +287 -0
  50. package/gsd-core/bin/lib/frontmatter.cjs +53 -6
  51. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +151 -0
  52. package/gsd-core/bin/lib/graphify.cjs +41 -9
  53. package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
  54. package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
  55. package/gsd-core/bin/lib/host-integration.cjs +61 -0
  56. package/gsd-core/bin/lib/init.cjs +34 -6
  57. package/gsd-core/bin/lib/milestone.cjs +56 -11
  58. package/gsd-core/bin/lib/phase-id.cjs +18 -0
  59. package/gsd-core/bin/lib/phase.cjs +51 -29
  60. package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
  61. package/gsd-core/bin/lib/plan-scan.cjs +9 -1
  62. package/gsd-core/bin/lib/probe-core.cjs +44 -4
  63. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  64. package/gsd-core/bin/lib/prohibition-enforcement.cjs +21 -19
  65. package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
  66. package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
  67. package/gsd-core/bin/lib/roadmap.cjs +45 -22
  68. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +18 -13
  69. package/gsd-core/bin/lib/runtime-name-policy.cjs +8 -10
  70. package/gsd-core/bin/lib/shell-command-projection.cjs +12 -8
  71. package/gsd-core/bin/lib/smart-entry.cjs +521 -0
  72. package/gsd-core/bin/lib/state-transition.cjs +15 -0
  73. package/gsd-core/bin/lib/state.cjs +27 -8
  74. package/gsd-core/bin/lib/surface.cjs +18 -9
  75. package/gsd-core/bin/lib/update-context.cjs +0 -5
  76. package/gsd-core/bin/lib/validate.cjs +2 -1
  77. package/gsd-core/bin/lib/verify.cjs +6 -4
  78. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
  79. package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
  80. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  81. package/gsd-core/bin/shared/model-catalog.json +8 -13
  82. package/gsd-core/bin/shared/runtime-aliases.manifest.json +0 -5
  83. package/gsd-core/references/agent-skills-bootstrap.md +60 -0
  84. package/gsd-core/references/gates.md +1 -1
  85. package/gsd-core/references/model-profiles.md +27 -0
  86. package/gsd-core/references/planning-config.md +1 -1
  87. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  88. package/gsd-core/workflows/add-backlog.md +1 -1
  89. package/gsd-core/workflows/add-phase.md +1 -1
  90. package/gsd-core/workflows/add-tests.md +1 -1
  91. package/gsd-core/workflows/add-todo.md +1 -1
  92. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  93. package/gsd-core/workflows/audit-fix.md +1 -1
  94. package/gsd-core/workflows/audit-milestone.md +1 -1
  95. package/gsd-core/workflows/audit-uat.md +1 -1
  96. package/gsd-core/workflows/autonomous.md +23 -25
  97. package/gsd-core/workflows/check-todos.md +1 -1
  98. package/gsd-core/workflows/cleanup.md +1 -1
  99. package/gsd-core/workflows/code-review-fix.md +1 -1
  100. package/gsd-core/workflows/code-review.md +1 -1
  101. package/gsd-core/workflows/complete-milestone.md +7 -11
  102. package/gsd-core/workflows/debug.md +1 -1
  103. package/gsd-core/workflows/diagnose-issues.md +1 -1
  104. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  105. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  106. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  107. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  108. package/gsd-core/workflows/discuss-phase.md +2 -2
  109. package/gsd-core/workflows/do.md +1 -1
  110. package/gsd-core/workflows/docs-update.md +1 -1
  111. package/gsd-core/workflows/edit-phase.md +1 -1
  112. package/gsd-core/workflows/eval-review.md +1 -1
  113. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  114. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  115. package/gsd-core/workflows/execute-phase.md +5 -6
  116. package/gsd-core/workflows/execute-plan.md +1 -1
  117. package/gsd-core/workflows/explore.md +1 -1
  118. package/gsd-core/workflows/extract-learnings.md +1 -1
  119. package/gsd-core/workflows/forensics.md +4 -4
  120. package/gsd-core/workflows/graduation.md +1 -1
  121. package/gsd-core/workflows/health.md +1 -1
  122. package/gsd-core/workflows/help/modes/full.md +15 -1
  123. package/gsd-core/workflows/help/modes/topic.md +1 -0
  124. package/gsd-core/workflows/import.md +1 -1
  125. package/gsd-core/workflows/ingest-docs.md +1 -1
  126. package/gsd-core/workflows/insert-phase.md +1 -1
  127. package/gsd-core/workflows/list-seeds.md +1 -1
  128. package/gsd-core/workflows/list-workspaces.md +1 -1
  129. package/gsd-core/workflows/manager.md +1 -1
  130. package/gsd-core/workflows/map-codebase.md +1 -1
  131. package/gsd-core/workflows/milestone-summary.md +4 -4
  132. package/gsd-core/workflows/mvp-phase.md +1 -1
  133. package/gsd-core/workflows/new-milestone.md +7 -1
  134. package/gsd-core/workflows/new-project.md +1 -1
  135. package/gsd-core/workflows/new-workspace.md +1 -1
  136. package/gsd-core/workflows/next.md +1 -1
  137. package/gsd-core/workflows/pause-work.md +1 -1
  138. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  139. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
  140. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
  141. package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
  142. package/gsd-core/workflows/plan-phase.md +12 -160
  143. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  144. package/gsd-core/workflows/plant-seed.md +1 -1
  145. package/gsd-core/workflows/pr-branch.md +1 -1
  146. package/gsd-core/workflows/profile-user.md +1 -1
  147. package/gsd-core/workflows/progress.md +1 -1
  148. package/gsd-core/workflows/quick.md +1 -1
  149. package/gsd-core/workflows/reapply-patches.md +1 -1
  150. package/gsd-core/workflows/remove-phase.md +1 -1
  151. package/gsd-core/workflows/remove-workspace.md +1 -1
  152. package/gsd-core/workflows/resume-project.md +1 -1
  153. package/gsd-core/workflows/review.md +50 -9
  154. package/gsd-core/workflows/scan.md +1 -1
  155. package/gsd-core/workflows/secure-phase.md +1 -1
  156. package/gsd-core/workflows/settings-advanced.md +13 -11
  157. package/gsd-core/workflows/settings-integrations.md +1 -1
  158. package/gsd-core/workflows/settings.md +1 -1
  159. package/gsd-core/workflows/ship.md +1 -1
  160. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  161. package/gsd-core/workflows/sketch.md +1 -1
  162. package/gsd-core/workflows/smart-entry.md +120 -0
  163. package/gsd-core/workflows/spec-phase.md +8 -6
  164. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  165. package/gsd-core/workflows/spike.md +1 -1
  166. package/gsd-core/workflows/stats.md +1 -1
  167. package/gsd-core/workflows/thread.md +1 -1
  168. package/gsd-core/workflows/transition.md +1 -1
  169. package/gsd-core/workflows/ui-phase.md +1 -1
  170. package/gsd-core/workflows/ui-review.md +1 -1
  171. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  172. package/gsd-core/workflows/update.md +3 -6
  173. package/gsd-core/workflows/validate-phase.md +1 -1
  174. package/gsd-core/workflows/verify-phase.md +1 -1
  175. package/gsd-core/workflows/verify-work.md +63 -6
  176. package/package.json +5 -5
  177. package/scripts/ci-test-scope.cjs +19 -1
  178. package/scripts/generate-package-identity.cjs +27 -2
  179. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -148
  180. package/scripts/lint-regression-test-names.allowlist.json +1 -273
  181. package/scripts/lint-test-file-count.allowlist.json +7 -91
  182. package/scripts/research-profiles.cjs +6 -6
  183. package/scripts/run-tests.cjs +67 -4
  184. package/scripts/slurm-adapter.cjs +269 -0
  185. package/scripts/sync-manifest-versions.cjs +67 -14
  186. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  187. package/skills/gsd-mempalace-recall/SKILL.md +9 -5
  188. package/skills/gsd-next/SKILL.md +30 -0
  189. package/skills/gsd-ns-workflow/SKILL.md +4 -1
  190. package/skills/gsd-review/SKILL.md +6 -0
  191. package/gemini-extension.json +0 -6
@@ -24,7 +24,7 @@ const PROFILES = [
24
24
  'Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.',
25
25
  color: 'cyan',
26
26
  tools:
27
- 'Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
27
+ 'Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
28
28
  requiredIncludes: [
29
29
  '@~/.claude/gsd-core/references/research-documentation-lookup.md',
30
30
  '@~/.claude/gsd-core/references/research-philosophy.md',
@@ -46,7 +46,7 @@ const PROFILES = [
46
46
  'Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.',
47
47
  color: 'cyan',
48
48
  tools:
49
- 'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
49
+ 'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
50
50
  requiredIncludes: [
51
51
  '@~/.claude/gsd-core/references/research-documentation-lookup.md',
52
52
  '@~/.claude/gsd-core/references/research-philosophy.md',
@@ -68,7 +68,7 @@ const PROFILES = [
68
68
  description:
69
69
  'Researches a single gray area decision and returns a structured comparison table with rationale. Spawned by discuss-phase advisor mode.',
70
70
  color: 'cyan',
71
- tools: 'Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*',
71
+ tools: 'Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*',
72
72
  requiredIncludes: [
73
73
  '@~/.claude/gsd-core/references/research-documentation-lookup.md',
74
74
  ],
@@ -84,7 +84,7 @@ const PROFILES = [
84
84
  'Researches a chosen AI framework\'s official docs to produce implementation-ready guidance — best practices, syntax, core patterns, and pitfalls distilled for the specific use case. Writes the Framework Quick Reference and Implementation Guidance sections of AI-SPEC.md. Spawned by /gsd:ai-integration-phase orchestrator.',
85
85
  color: 'green',
86
86
  tools:
87
- 'Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__*',
87
+ 'Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__*, mcp__plugin_context7_context7__*',
88
88
  requiredIncludes: [
89
89
  '@~/.claude/gsd-core/references/research-documentation-lookup.md',
90
90
  ],
@@ -101,7 +101,7 @@ const PROFILES = [
101
101
  'Researches the business domain and real-world application context of the AI system being built. Surfaces domain expert evaluation criteria, industry-specific failure modes, regulatory context, and what "good" looks like for practitioners in this field — before the eval-planner turns it into measurable rubrics. Spawned by /gsd:ai-integration-phase orchestrator.',
102
102
  color: 'purple',
103
103
  tools:
104
- 'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*',
104
+ 'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*',
105
105
  requiredIncludes: [
106
106
  '@~/.claude/gsd-core/references/research-documentation-lookup.md',
107
107
  ],
@@ -117,7 +117,7 @@ const PROFILES = [
117
117
  'Produces UI-SPEC.md design contract for frontend phases. Reads upstream artifacts, detects design system state, asks only unanswered questions. Spawned by /gsd:ui-phase orchestrator.',
118
118
  color: 'purple',
119
119
  tools:
120
- 'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
120
+ 'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
121
121
  requiredIncludes: [
122
122
  '@~/.claude/gsd-core/references/research-documentation-lookup.md',
123
123
  ],
@@ -119,6 +119,51 @@ function ensureBuiltArtifacts(overrides = {}) {
119
119
  }
120
120
  }
121
121
  }
122
+
123
+ // hooks/dist/ is gitignored (.gitignore) and NOT built by `prepare`
124
+ // (npm run build:lib only) — only the full `build`/`prepublishOnly` scripts run
125
+ // build:hooks. So on a clean checkout + `npm ci` (fresh CI, incl. the scoped
126
+ // test lane) hooks/dist starts absent. Install tests (e.g.
127
+ // bug-3683-workflow-colon-namespace-leak) spawn `install.js --<runtime> --local`
128
+ // which copies hooks from hooks/dist/ and then verifyInstalled() hard-fails if
129
+ // the target hooks dir is empty. build-hooks.js `build()` creates DIST_DIR
130
+ // empty and fills it file-by-file, so the FIRST on-demand build (triggered by
131
+ // whichever concurrent install test's before() hook runs first) exposes a
132
+ // window where hooks/dist exists but is empty/partial. A concurrently-spawned
133
+ // install reader observes zero hooks -> "Failed to install hooks: directory is
134
+ // empty" -> intermittent scoped-lane failure (full lanes dodge it only by luck
135
+ // of a hooks-builder finishing early). Building hooks/dist ONCE here — the same
136
+ // upfront chokepoint as ensureBuiltArtifacts, single-process with no concurrent
137
+ // readers — fully populates dist before any test runs, closing the first-build
138
+ // empty window everywhere (CI scoped/unit shards + local). Subsequent on-demand
139
+ // rebuilds only atomically replace individual files (per-file rename in
140
+ // build-hooks.js) and never re-empty the dir, so they stay safe.
141
+ function ensureBuiltHooks(overrides = {}) {
142
+ const { existsSync, statSync } = require('fs');
143
+ const root = overrides.root || join(__dirname, '..');
144
+ const distDir = overrides.distDir || join(root, 'hooks', 'dist');
145
+ const hookNames = overrides.hookNames || require('./build-hooks.js').HOOKS_TO_COPY;
146
+ const runBuild = overrides.runBuild || (() => {
147
+ execFileSync(process.execPath, [join(root, 'scripts', 'build-hooks.js')], {
148
+ cwd: root,
149
+ stdio: 'inherit',
150
+ });
151
+ });
152
+
153
+ // dist is "complete" only if every expected hook exists as a non-empty file.
154
+ // Absent dir, empty dir, or a missing/zero-byte hook all trigger a rebuild.
155
+ const complete = existsSync(distDir) && hookNames.every((hook) => {
156
+ const p = join(distDir, hook);
157
+ try {
158
+ return existsSync(p) && statSync(p).size > 0;
159
+ } catch {
160
+ return false;
161
+ }
162
+ });
163
+ if (!complete) {
164
+ runBuild();
165
+ }
166
+ }
122
167
  const MARKED_SUITES = ['integration', 'install', 'security', 'slow'];
123
168
 
124
169
  // Recursively collect *.test.cjs files under dir, returning paths relative to dir.
@@ -454,6 +499,11 @@ function main() {
454
499
  // Build the gitignored bin/lib artifact if absent, before any test requires it.
455
500
  ensureBuiltArtifacts();
456
501
 
502
+ // Build the gitignored hooks/dist artifact once, before any concurrent install
503
+ // test spawns install.js and reads it — closes the first-build empty-dir race
504
+ // that intermittently failed the scoped CI lane (see ensureBuiltHooks above).
505
+ ensureBuiltHooks();
506
+
457
507
  // Hermeticity: in-process tests resolve `.planning` via planningDir(cwd), which
458
508
  // honours GSD_PROJECT/GSD_WORKSTREAM. A developer shell inside a GSD workstream
459
509
  // exports GSD_WORKSTREAM, which would redirect fixture STATE.md reads away from
@@ -512,9 +562,19 @@ function main() {
512
562
  const MAX_CMDLINE_CHARS = process.env.RUN_TESTS_MAX_CMDLINE_CHARS
513
563
  ? Number(process.env.RUN_TESTS_MAX_CMDLINE_CHARS)
514
564
  : 28000; // headroom below the 32,767 Windows ceiling
565
+ // A full-lane shard (~171 files) fit in ONE chunk at the old cap of 180, so the
566
+ // entire shard's wall-clock ran against a single per-chunk timeout. On the slow
567
+ // Windows runner the install-heavy files in a shard (e.g. install-minimal-hooks
568
+ // .test.cjs alone runs ~250 cases doing dozens of real installs) push that single
569
+ // chunk past the 600s per-chunk backstop — killed mid-run while still making slow
570
+ // progress (verified: no leaked handle / hang; --test-force-exit exits leaks
571
+ // cleanly, so the timeout was pure slowness, NOT the leak the kill message guesses).
572
+ // The per-chunk timeout is sized for a "healthy chunk (~4-5 min)"; keep chunks at
573
+ // roughly half a shard so each gets its own fresh 600s budget and a fresh node
574
+ // process (also relieving per-process memory pressure from 170+ files at once).
515
575
  const MAX_FILES_PER_CHUNK = process.env.RUN_TESTS_MAX_FILES_PER_CHUNK
516
576
  ? Number(process.env.RUN_TESTS_MAX_FILES_PER_CHUNK)
517
- : 180;
577
+ : 90;
518
578
 
519
579
  // node:test does not exit until the event loop drains. A unit test that leaks
520
580
  // an open handle (un-terminated Worker, un-killed child_process, ref'd timer)
@@ -580,9 +640,12 @@ function main() {
580
640
  if (timedOut) {
581
641
  console.error(
582
642
  `run-tests: chunk ${i + 1}/${chunks.length} exceeded the per-chunk timeout ` +
583
- `of ${chunkTimeoutMs}ms and was killed — a test in this chunk is likely leaking ` +
643
+ `of ${chunkTimeoutMs}ms and was killed. Two possible causes: (1) a test leaks ` +
584
644
  `an open handle (un-terminated Worker, un-killed child process, or ref'd timer) ` +
585
- `so node --test never exits. Files: ${chunks[i]
645
+ `so node --test never exits — but --test-force-exit already guards that, so if it ` +
646
+ `is enabled suspect (2) the chunk is legitimately too slow for the budget (too ` +
647
+ `many/too-heavy files packed together). Check whether output kept flowing until ` +
648
+ `the kill (slow) vs stopped early (hang) before assuming a leak. Files: ${chunks[i]
586
649
  .map(f => f.split(/[\\/]/).pop())
587
650
  .join(' ')}`,
588
651
  );
@@ -600,4 +663,4 @@ if (require.main === module) {
600
663
  runMain(main);
601
664
  }
602
665
 
603
- module.exports = { suiteOf, ensureBuiltArtifacts, parseShardArg, selectShard };
666
+ module.exports = { suiteOf, ensureBuiltArtifacts, ensureBuiltHooks, parseShardArg, selectShard };
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * slurm-adapter.cjs — SLURM scheduler-adapter helper for the external-job
6
+ * capability (#1164 / #1105).
7
+ *
8
+ * Thin CLI: runs bounded sbatch / squeue / sacct subprocesses and delegates
9
+ * all parsing, manifest build/validate, and fail-closed writing to the pure
10
+ * module (gsd-core/bin/lib/external-job.cjs). The pure module is fully unit-
11
+ * tested; this script is the operator surface that needs a real cluster.
12
+ *
13
+ * Subcommands:
14
+ * submit --plan <plan_id> --phase <phase> --expected <path>[,<path>] \
15
+ * --verify <cmd> --resume <cmd> -- <sbatch...>
16
+ * poll --job <job_id> [--plan <plan_id>]
17
+ * show --job <job_id> (surface manifest status + commands; no auto-run)
18
+ *
19
+ * Trust boundary: this script never auto-runs verification_command or
20
+ * resume_command from a manifest (planning-artifacts.md). `show` prints them
21
+ * for explicit operator confirmation.
22
+ */
23
+
24
+ const { execFileSync } = require('node:child_process');
25
+ const fs = require('node:fs');
26
+ const path = require('node:path');
27
+
28
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
29
+ const m = require('../gsd-core/bin/lib/external-job.cjs');
30
+
31
+ // #1164 refinements B + C: the adapter now resolves external_job.* settings
32
+ // through the canonical capability-config seam (resolveConfigKey in
33
+ // capability-activation.cjs), which walks loadConfig -> workstream config.json
34
+ // -> root config.json -> registry configSchema default. Env vars remain the
35
+ // top-precedence override so cluster operators can tune without editing config.
36
+ const { resolveConfigKey } = require('../gsd-core/bin/lib/capability-activation.cjs');
37
+ const FALLBACK_SUBMIT_TIMEOUT_MS = 30000;
38
+ const FALLBACK_POLL_TIMEOUT_MS = 15000;
39
+ const FALLBACK_ARTIFACT_DIR = 'Artifacts/jobs';
40
+
41
+ /**
42
+ * Resolve the external_job.* runtime settings. Pure given {config, env,
43
+ * registry}; degrades to hardcoded fallbacks if the registry/config are absent
44
+ * so the adapter never unbounds a subprocess (CLAUDE.md bounded-subprocess).
45
+ *
46
+ * Precedence: env override > nested config value > registry default > fallback.
47
+ */
48
+ function resolveExternalJobSettings({ cwd, env, config, registry } = {}) {
49
+ const reg = registry || {};
50
+ let cfg = config;
51
+ if (cfg === undefined) {
52
+ try {
53
+ cfg = require('../gsd-core/bin/lib/config-loader.cjs').loadConfig(cwd);
54
+ } catch {
55
+ cfg = {}; // resolveConfigKey still falls back to the registry default
56
+ }
57
+ }
58
+ const e = env || {};
59
+ const submitEnv = e.GSD_SLURM_SUBMIT_TIMEOUT_MS;
60
+ const pollEnv = e.GSD_SLURM_POLL_TIMEOUT_MS;
61
+ const artifactEnv = e.GSD_EXTERNAL_JOB_ARTIFACT_DIR;
62
+ const submit = submitEnv ? Number(submitEnv)
63
+ : _num(resolveConfigKey('external_job.submit_timeout_ms', { config: cfg, cwd, registry: reg }), FALLBACK_SUBMIT_TIMEOUT_MS);
64
+ const poll = pollEnv ? Number(pollEnv)
65
+ : _num(resolveConfigKey('external_job.poll_timeout_ms', { config: cfg, cwd, registry: reg }), FALLBACK_POLL_TIMEOUT_MS);
66
+ const artifactDir = artifactEnv
67
+ || _str(resolveConfigKey('external_job.artifact_dir', { config: cfg, cwd, registry: reg }), FALLBACK_ARTIFACT_DIR);
68
+ return { submitTimeoutMs: submit, pollTimeoutMs: poll, artifactDir };
69
+ }
70
+
71
+ function _num(res, fallback) {
72
+ const v = res && res.found ? res.value : undefined;
73
+ return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
74
+ }
75
+ function _str(res, fallback) {
76
+ const v = res && res.found ? res.value : undefined;
77
+ return typeof v === 'string' && v.length > 0 ? v : fallback;
78
+ }
79
+
80
+ function usage() {
81
+ return [
82
+ 'usage: slurm-adapter.cjs <submit|poll|show> ...',
83
+ ' submit --plan <id> --phase <n> --expected <p1,p2> --verify <cmd> --resume <cmd> -- sbatch --parsable ...',
84
+ ' poll --job <job_id> [--plan <plan_id>]',
85
+ ' show --job <job_id>',
86
+ ].join('\n');
87
+ }
88
+
89
+ function parseFlags(argv) {
90
+ const out = {};
91
+ const rest = [];
92
+ for (let i = 0; i < argv.length; i++) {
93
+ const a = argv[i];
94
+ if (a === '--') { out['--'] = argv.slice(i + 1); break; }
95
+ if (a.startsWith('--')) {
96
+ const v = argv[i + 1];
97
+ out[a.slice(2)] = v;
98
+ i++;
99
+ } else {
100
+ rest.push(a);
101
+ }
102
+ }
103
+ out._ = rest;
104
+ return out;
105
+ }
106
+
107
+ function findPlanningDir(start) {
108
+ let dir = path.resolve(start || process.cwd());
109
+ for (let i = 0; i < 10; i++) {
110
+ if (fs.existsSync(path.join(dir, '.planning'))) return path.join(dir, '.planning');
111
+ const parent = path.dirname(dir);
112
+ if (parent === dir) break;
113
+ dir = parent;
114
+ }
115
+ throw new ExitError(1, 'could not locate a .planning directory (walked up 10 levels)');
116
+ }
117
+
118
+ function cmdSubmit(flags) {
119
+ const plan = flags.plan;
120
+ const phase = flags.phase;
121
+ const sbatchCmd = flags['--'];
122
+ if (!plan || phase === undefined || !Array.isArray(sbatchCmd) || sbatchCmd.length === 0) {
123
+ throw new ExitError(1, 'submit requires --plan, --phase, and an sbatch command after --\n' + usage());
124
+ }
125
+ const expected = (flags.expected || '').split(',').map((s) => s.trim()).filter(Boolean);
126
+ if (expected.length === 0) throw new ExitError(1, 'submit requires --expected (comma-separated artifact paths)');
127
+ const verify = flags.verify;
128
+ const resume = flags.resume || ('/gsd:execute-phase ' + phase);
129
+ if (!verify) throw new ExitError(1, 'submit requires --verify (the command that verifies job output)');
130
+
131
+ // Resolve settings from config/env before any subprocess (env > config > default).
132
+ const planningDir = findPlanningDir();
133
+ const settings = resolveExternalJobSettings({
134
+ cwd: path.dirname(planningDir),
135
+ env: process.env,
136
+ });
137
+
138
+ let stdout;
139
+ try {
140
+ stdout = execFileSync(sbatchCmd[0], sbatchCmd.slice(1), {
141
+ encoding: 'utf8',
142
+ timeout: settings.submitTimeoutMs,
143
+ maxBuffer: 1024 * 1024,
144
+ });
145
+ } catch (e) {
146
+ throw new ExitError(1, 'sbatch failed: ' + (e.message || String(e)));
147
+ }
148
+ const parsed = m.parseSbatchParsable(stdout);
149
+ if (!parsed.ok) {
150
+ throw new ExitError(1, 'could not parse sbatch --parsable output (kind=' + parsed.kind + '): ' + parsed.raw);
151
+ }
152
+ const manifest = m.buildManifest({
153
+ plan_id: plan,
154
+ phase,
155
+ job_id: parsed.job_id,
156
+ backend: 'slurm',
157
+ submit_command: sbatchCmd.join(' '),
158
+ status: 'submitted',
159
+ expected_artifacts: expected,
160
+ verification_command: verify,
161
+ resume_command: resume,
162
+ });
163
+ const res = m.writeManifest(manifest, planningDir);
164
+ if (!res.ok) {
165
+ throw new ExitError(1, 'writeManifest refused (' + res.kind + '): ' + res.message);
166
+ }
167
+ process.stdout.write('submitted job ' + parsed.job_id + ' for plan ' + plan + '\n');
168
+ process.stdout.write('manifest: ' + res.path + '\n');
169
+ process.stdout.write('artifact_dir: ' + settings.artifactDir + '\n');
170
+ process.stdout.write('state: external_job_waiting (SUMMARY deferred)\n');
171
+ }
172
+
173
+ function cmdPoll(flags) {
174
+ const jobId = flags.job;
175
+ if (!jobId) throw new ExitError(1, 'poll requires --job <job_id>\n' + usage());
176
+ const planningDir = findPlanningDir();
177
+ const settings = resolveExternalJobSettings({
178
+ cwd: path.dirname(planningDir),
179
+ env: process.env,
180
+ });
181
+ const manifestFile = m.manifestPath(planningDir, jobId);
182
+ if (!fs.existsSync(manifestFile)) {
183
+ throw new ExitError(1, 'no manifest for job ' + jobId + ' at ' + manifestFile);
184
+ }
185
+ const existing = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
186
+
187
+ let rawState = null;
188
+ try {
189
+ const out = execFileSync('squeue', ['-h', '-j', jobId, '-o', '%i %T'], {
190
+ encoding: 'utf8', timeout: settings.pollTimeoutMs, maxBuffer: 1024 * 1024,
191
+ }).trim();
192
+ const line = out.split('\n')[0];
193
+ const parsed = m.parseSqueueLine(line || '');
194
+ if (parsed) rawState = parsed.state;
195
+ } catch (_e) { /* squeue empty/failed — fall back to sacct */ }
196
+ if (!rawState) {
197
+ try {
198
+ const out = execFileSync('sacct', ['-X', '-P', '-j', jobId, '-o', 'JobID,State'], {
199
+ encoding: 'utf8', timeout: settings.pollTimeoutMs, maxBuffer: 1024 * 1024,
200
+ }).trim();
201
+ for (const line of out.split('\n').slice(1)) {
202
+ const parsed = m.parseSacctRow(line.split('|'));
203
+ if (parsed) { rawState = parsed.state; break; }
204
+ }
205
+ } catch (e) {
206
+ throw new ExitError(1, 'both squeue and sacct failed: ' + (e.message || String(e)));
207
+ }
208
+ }
209
+ const mapped = m.mapSlurmState(rawState || '');
210
+ if (!mapped) throw new ExitError(1, 'unmapped SLURM state "' + rawState + '" — not guessing; inspect manually');
211
+
212
+ const updated = m.buildManifest(
213
+ Object.assign({}, existing, { status: mapped, terminal_details: existing.terminal_details || null }),
214
+ { clock: { nowIso: () => existing.submitted_at } },
215
+ );
216
+ const res = m.writeManifest(updated, planningDir);
217
+ if (!res.ok) throw new ExitError(1, 'writeManifest refused (' + res.kind + '): ' + res.message);
218
+ process.stdout.write(JSON.stringify({ job_id: jobId, slurm_state: rawState, manifest_status: mapped, path: res.path }) + '\n');
219
+ }
220
+
221
+ function formatShowReport(manifest) {
222
+ const lines = [];
223
+ lines.push('job ' + manifest.job_id + ' (plan ' + manifest.plan_id + ', backend ' + manifest.backend + ')');
224
+ lines.push('status: ' + manifest.status);
225
+ if (manifest.terminal_details) {
226
+ lines.push('terminal_details: ' + JSON.stringify(manifest.terminal_details));
227
+ }
228
+ // Trust boundary: surface commands for confirmation, never auto-run.
229
+ lines.push('');
230
+ lines.push('Manifest commands (UNTRUSTED — confirm before running):');
231
+ lines.push(' submit_command: ' + manifest.submit_command);
232
+ lines.push(' verification_command: ' + manifest.verification_command);
233
+ lines.push(' resume_command: ' + manifest.resume_command);
234
+ return lines.join('\n') + '\n';
235
+ }
236
+
237
+ function cmdShow(flags) {
238
+ const jobId = flags.job;
239
+ if (!jobId) throw new ExitError(1, 'show requires --job <job_id>\n' + usage());
240
+ const planningDir = findPlanningDir();
241
+ const manifestFile = m.manifestPath(planningDir, jobId);
242
+ if (!fs.existsSync(manifestFile)) {
243
+ throw new ExitError(1, 'no manifest for job ' + jobId + ' at ' + manifestFile);
244
+ }
245
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
246
+ process.stdout.write(formatShowReport(manifest));
247
+ }
248
+
249
+ function main() {
250
+ const [, , sub, ...rest] = process.argv;
251
+ const flags = parseFlags(rest);
252
+ if (sub === 'submit') cmdSubmit(flags);
253
+ else if (sub === 'poll') cmdPoll(flags);
254
+ else if (sub === 'show') cmdShow(flags);
255
+ else throw new ExitError(1, usage());
256
+ return 0;
257
+ }
258
+
259
+ if (require.main === module) {
260
+ runMain(main);
261
+ }
262
+
263
+ module.exports = {
264
+ parseFlags,
265
+ findPlanningDir,
266
+ resolveExternalJobSettings,
267
+ formatShowReport,
268
+ ExitError,
269
+ };
@@ -21,18 +21,67 @@ const { execFileSync } = require('child_process');
21
21
 
22
22
  const ROOT = path.resolve(__dirname, '..');
23
23
 
24
- // Single source of truth: runtime-integration manifests whose top-level `version`
25
- // MUST track package.json. Add a new manifest here so `npm version` keeps it in
26
- // sync — the regression guard test (issue 844) fails if you forget.
24
+ // Single source of truth: runtime-integration manifests whose `version` field
25
+ // MUST track package.json. Each entry names the manifest path plus the dotted
26
+ // path to the version field inside that JSON document. Top-level `version` is
27
+ // the default (plugin.json); the Claude plugin
28
+ // marketplace manifest carries its canonical version at plugins[0].version
29
+ // (the schema-canonical location runtimes read — issue #1855).
30
+ //
31
+ // Add a new manifest here so `npm version` keeps it in sync — the regression
32
+ // guard test (issue 844) fails if you forget.
33
+ // #1928: gemini-extension.json was removed with the gemini runtime (Google
34
+ // sunset Gemini CLI 2026-06-18); it is no longer a registered manifest.
27
35
  const VERSIONED_MANIFESTS = [
28
- '.claude-plugin/plugin.json',
29
- 'gemini-extension.json',
36
+ { path: '.claude-plugin/plugin.json', versionKey: 'version' },
37
+ { path: '.claude-plugin/marketplace.json', versionKey: 'plugins.0.version' },
38
+ { path: 'vscode/package.json', versionKey: 'version' },
30
39
  ];
31
40
 
41
+ // Convenience: just the registered paths, for consumers that only need to
42
+ // iterate files (e.g. the issue-844 regression-guard ALLOWED set).
43
+ const VERSIONED_MANIFEST_PATHS = VERSIONED_MANIFESTS.map((e) => e.path);
44
+
32
45
  function readJson(p) {
33
46
  return JSON.parse(fs.readFileSync(p, 'utf8'));
34
47
  }
35
48
 
49
+ // Property names that must never be traversed/assigned through a dotted path —
50
+ // the prototype-pollution triple. Mirrors the _isSafePropKey CodeQL barrier used
51
+ // elsewhere in the repo. Versions are hand-authored descriptors, but the helpers
52
+ // are exported, so guard them before reuse across a trust boundary.
53
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
54
+ function assertSafePath(dotPath) {
55
+ for (const seg of String(dotPath).split('.')) {
56
+ if (UNSAFE_KEYS.has(seg)) {
57
+ throw new Error(`refusing to traverse reserved property "${seg}" in version path "${dotPath}"`);
58
+ }
59
+ }
60
+ }
61
+
62
+ // Read a dotted path ('plugins.0.version') from a parsed JSON document.
63
+ // Returns undefined if any intermediate is missing. Array indices are plain
64
+ // numeric keys, so 'plugins.0.version' resolves obj.plugins[0].version.
65
+ // Reserved properties (__proto__/constructor/prototype) are rejected.
66
+ function getByPath(obj, dotPath) {
67
+ assertSafePath(dotPath);
68
+ return String(dotPath).split('.').reduce((cur, key) => (cur == null ? cur : cur[key]), obj);
69
+ }
70
+
71
+ // Write a value at a dotted path. Intermediate nodes must already exist (every
72
+ // registered manifest is hand-authored with its version slot present, so this
73
+ // never needs to materialize a path). Reserved properties are rejected. Kept
74
+ // trivial — no eval, no creation.
75
+ function setByPath(obj, dotPath, value) {
76
+ assertSafePath(dotPath);
77
+ const parts = String(dotPath).split('.');
78
+ let cur = obj;
79
+ for (let i = 0; i < parts.length - 1; i++) {
80
+ cur = cur[parts[i]];
81
+ }
82
+ cur[parts[parts.length - 1]] = value;
83
+ }
84
+
36
85
  function getPackageVersion(root) {
37
86
  const r = root || ROOT;
38
87
  return readJson(path.join(r, 'package.json')).version;
@@ -44,13 +93,13 @@ function syncManifestVersions(opts) {
44
93
  const root = (opts && opts.root) || ROOT;
45
94
  const v = (opts && opts.version) != null ? opts.version : getPackageVersion(root);
46
95
  const changed = [];
47
- for (const rel of VERSIONED_MANIFESTS) {
48
- const abs = path.join(root, rel);
96
+ for (const entry of VERSIONED_MANIFESTS) {
97
+ const abs = path.join(root, entry.path);
49
98
  const manifest = readJson(abs);
50
- if (manifest.version !== v) {
51
- manifest.version = v;
99
+ if (getByPath(manifest, entry.versionKey) !== v) {
100
+ setByPath(manifest, entry.versionKey, v);
52
101
  fs.writeFileSync(abs, JSON.stringify(manifest, null, 2) + '\n');
53
- changed.push(rel);
102
+ changed.push(entry.path);
54
103
  }
55
104
  }
56
105
  return changed;
@@ -61,9 +110,10 @@ function findDrift(opts) {
61
110
  const root = (opts && opts.root) || ROOT;
62
111
  const v = (opts && opts.version) != null ? opts.version : getPackageVersion(root);
63
112
  const drift = [];
64
- for (const rel of VERSIONED_MANIFESTS) {
65
- const found = readJson(path.join(root, rel)).version;
66
- if (found !== v) drift.push({ manifest: rel, found, expected: v });
113
+ for (const entry of VERSIONED_MANIFESTS) {
114
+ const manifest = readJson(path.join(root, entry.path));
115
+ const found = getByPath(manifest, entry.versionKey);
116
+ if (found !== v) drift.push({ manifest: entry.path, found, expected: v });
67
117
  }
68
118
  return drift;
69
119
  }
@@ -143,7 +193,7 @@ function stageManifests(opts) {
143
193
  console.warn('sync-manifest-versions: not a git work tree; skipping staging.');
144
194
  return;
145
195
  }
146
- const toStage = [...VERSIONED_MANIFESTS, ...listCapabilityManifests({ root })];
196
+ const toStage = [...VERSIONED_MANIFEST_PATHS, ...listCapabilityManifests({ root })];
147
197
  try {
148
198
  execFileSync('git', ['add', '--', ...toStage], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
149
199
  } catch (err) {
@@ -154,6 +204,9 @@ function stageManifests(opts) {
154
204
 
155
205
  module.exports = {
156
206
  VERSIONED_MANIFESTS,
207
+ VERSIONED_MANIFEST_PATHS,
208
+ getByPath,
209
+ setByPath,
157
210
  syncManifestVersions,
158
211
  findDrift,
159
212
  getPackageVersion,
@@ -56,7 +56,7 @@ On any error or timeout, stop and let the phase continue -- capture is best-effo
56
56
 
57
57
  1. **Dedup first.** Interactive: `mempalace_check_duplicate` on the artifact's deterministic drawer id. Headless: rely on `mempalace mine`'s content-hash idempotency.
58
58
  2. **Add the drawer (verbatim).** File the exact artifact text into `room: <room>` of `wing: <wing>` with provenance (`source_file`, phase id). Interactive: `mempalace_add_drawer`. Headless: `mempalace mine <path> --wing <wing> --room <room>`.
59
- 3. **Mirror KG facts** when `config.mempalace.mirror_kg` is true: extract decision/delivery facts and `mempalace_kg_add` them with `valid_from` = the phase date (e.g. `(<project>, decided, <decision>)` from CONTEXT; `(<phase>, delivered, <capability>)` from SUMMARY). Only `augment` is currently wired, so these are an *additive* mirror of `.planning/graphs/`. (`kg_backend`/`replace` are forward-declared and behave as `augment` today.)
59
+ 3. **Mirror KG facts** when `config.mempalace.mirror_kg` is true: extract decision/delivery facts and `mempalace_kg_add` them with `valid_from` = the phase date (e.g. `(<project>, decided, <decision>)` from CONTEXT; `(<phase>, delivered, <capability>)` from SUMMARY). Under `augment` these are an *additive* mirror of GSD's native `.planning/graphs/`. Under `kg_backend`/`replace` the palace KG is the *authoritative* fact store — GSD still produces `.planning/graphs/` through its normal graphify, so an unreachable palace never loses a fact.
60
60
  4. Re-running a phase MUST NOT create duplicate drawers (deterministic ids + `check_duplicate`).
61
61
 
62
62
  ## Step 4 -- Report
@@ -48,13 +48,17 @@ This step is `onError: skip` at `plan:pre` -- recall never blocks planning.
48
48
  ## Step 2 -- Resolve wing, mode, and transport
49
49
 
50
50
  1. **Wing.** Use `config.mempalace.wing` if non-empty; otherwise derive from `config.project_code`; otherwise fall back to the repository directory name.
51
- 2. **Mode.** Read `config.mempalace.memory_mode` (`augment` | `kg_backend` | `replace`, default `augment`). Only `augment` is wired today, so recall always treats the palace as additive; `kg_backend`/`replace` are forward-declared and behave as `augment`.
51
+ 2. **Mode.** Read `config.mempalace.memory_mode` (`augment` | `kg_backend` | `replace`, default `augment`). It sets how authoritative the palace is during recall:
52
+ - `augment` — the palace is an *additional* layer; read native memory (`.planning/graphs/`, STATE) too and treat the palace as supplementary.
53
+ - `kg_backend` — for knowledge-graph facts, query the palace's temporal KG *first, as the primary source*; fall back to `.planning/graphs/` when the palace is unreachable. Non-KG drawer recall stays additive.
54
+ - `replace` — resolve recall *through the palace as the source of truth*; consult native artifacts only as a fallback when the palace is unreachable.
55
+ In every mode an unreachable palace degrades to native memory — recall never blocks (`onError: skip`).
52
56
  3. **Transport.** Prefer the **MCP tools** (`mempalace_*`) in interactive runs *when your MemPalace MCP server is registered and your runtime permits those tools*. Otherwise — headless/cron/autonomous runs, or runtimes that don't grant the MemPalace MCP tools — use the **CLI** (`mempalace wake-up`, `mempalace search`), which this skill's `Bash` allow-tool always covers. If neither is reachable, go to Step 4.
53
57
  4. **Topic.** Read the phase `CONTEXT.md` (the consumed artifact). Derive a short search query from its title, goal, and key decisions.
54
58
 
55
59
  ## Step 3 -- Retrieve (read-only)
56
60
 
57
- All calls in this step are side-effect-free. On any error or timeout, stop retrieving and write whatever was gathered (or the stub) -- never raise.
61
+ All calls in this step are side-effect-free. On any error or timeout, stop retrieving and write whatever was gathered (or the stub) -- never raise. This skill reads only the palace; GSD's planner reads native memory (`.planning/graphs/`, STATE) regardless. So under `kg_backend`/`replace` an unreachable palace falls back to that native memory automatically — reflect that in the stub (Step 4) rather than implying memory is gone.
58
62
 
59
63
  1. **Wake up** (cheap, ~600--900 tokens):
60
64
  - Interactive: read the wing identity/summary, then `mempalace_search`.
@@ -62,7 +66,7 @@ All calls in this step are side-effect-free. On any error or timeout, stop retri
62
66
  2. **Targeted search:**
63
67
  - Interactive: `mempalace_search(query=<topic>, wing=<wing>)`.
64
68
  - Headless: `mempalace search "<topic>" --wing <wing>`.
65
- 3. **Knowledge-graph facts** (when `config.mempalace.mirror_kg` is true): `mempalace_kg_query` / `mempalace_kg_timeline` for decisions relevant to the topic and their validity windows. Only `augment` is currently wired, so the palace KG *supplements* GSD's native `.planning/graphs/` — do not treat it as the sole source. (`kg_backend`/`replace` are forward-declared and behave as `augment` today.)
69
+ 3. **Knowledge-graph facts** (when `config.mempalace.mirror_kg` is true): `mempalace_kg_query` / `mempalace_kg_timeline` for decisions relevant to the topic and their validity windows. Under `augment` the palace KG *supplements* GSD's native `.planning/graphs/` — combine both, do not treat the palace as the sole source. Under `kg_backend` or `replace` the palace KG is the *primary* graph source — query it first and use `.planning/graphs/` only as a fallback when the palace is unreachable.
66
70
  4. **Dedup** the returned drawers/facts; keep the top results.
67
71
 
68
72
  ## Step 4 -- Write MEMORY-RECALL.md
@@ -86,12 +90,12 @@ _Wing: <wing> · Mode: <mode> · Transport: <mcp|cli>_
86
90
  - <surprise> — <provenance>
87
91
  ```
88
92
 
89
- When MemPalace is unreachable, write the stub and continue:
93
+ When MemPalace is unreachable, write the stub and continue. Under `kg_backend`/`replace`, name the native fallback so the planner knows memory is not gone — only un-augmented by the palace this run:
90
94
 
91
95
  ```markdown
92
96
  # Memory Recall (MemPalace)
93
97
 
94
- _MemPalace unavailable at recall time — proceeding without recalled memory._
98
+ _MemPalace unavailable at recall time — falling back to GSD native memory (`.planning/graphs/`, STATE). No palace recall this run._
95
99
  ```
96
100
 
97
101
  ## Anti-Patterns
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: gsd-next
3
+ description: "Smart entry — detect project state and route to the right next GSD action."
4
+ effort: low
5
+ allowed-tools:
6
+ - Read
7
+ - Bash
8
+ - Glob
9
+ - SlashCommand
10
+ - AskUserQuestion
11
+ ---
12
+
13
+ <objective>
14
+ GSD smart entry — the state-aware front door. Detect what's going on in this project, then present a short menu of the right next actions and dispatch to one.
15
+
16
+ This is a launcher/router only. It never does the work itself. It reads project + workflow state via `gsd-tools smart-entry --json`, shows a situation-appropriate menu, and hands off to an existing GSD command.
17
+ </objective>
18
+
19
+ <execution_context>
20
+ @~/.claude/gsd-core/workflows/smart-entry.md
21
+ @~/.claude/gsd-core/references/ui-brand.md
22
+ </execution_context>
23
+
24
+ <context>
25
+ Arguments: $ARGUMENTS
26
+ </context>
27
+
28
+ <process>
29
+ Follow ~/.claude/gsd-core/workflows/smart-entry.md. Detect the situation, present the menu, and dispatch exactly one command. Then stop.
30
+ </process>
@@ -10,7 +10,9 @@ allowed-tools:
10
10
  Route to the appropriate phase-pipeline skill based on the user's intent.
11
11
  Sub-skill names below are post-#2790 consolidated targets — `gsd-phase`
12
12
  absorbs the former add/insert/remove/edit-phase commands and `gsd-progress`
13
- absorbs the former next/do commands.
13
+ absorbs the former next/do workflow-advance commands. The reclaimed
14
+ `gsd-next` target is the state-aware smart-entry launcher, not the retired
15
+ workflow-advance command.
14
16
 
15
17
  | User wants | Invoke |
16
18
  |---|---|
@@ -21,6 +23,7 @@ absorbs the former next/do commands.
21
23
  | Verify built features through UAT | gsd-verify-work |
22
24
  | Add / insert / remove / edit a phase | gsd-phase |
23
25
  | Advance to the next logical step | gsd-progress |
26
+ | Open the state-aware smart-entry launcher | gsd-next |
24
27
  | Offload planning to the ultraplan cloud | gsd-ultraplan-phase |
25
28
  | Cross-AI plan review convergence loop | gsd-plan-review-convergence |
26
29
  | Generate tests for a completed phase | gsd-add-tests |