@opengsd/gsd-core 1.5.0-rc.2 → 1.5.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 (69) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-mempalace-curator.md +47 -0
  3. package/bin/install.js +25 -7
  4. package/commands/gsd/mempalace-capture.md +71 -0
  5. package/commands/gsd/mempalace-recall.md +102 -0
  6. package/commands/gsd/ns-context.md +4 -2
  7. package/gemini-extension.json +1 -1
  8. package/gsd-core/bin/gsd-tools.cjs +110 -92
  9. package/gsd-core/bin/lib/capability-activation.cjs +86 -0
  10. package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
  11. package/gsd-core/bin/lib/capability-state.cjs +31 -18
  12. package/gsd-core/bin/lib/check-command-router.cjs +328 -1
  13. package/gsd-core/bin/lib/clusters.cjs +2 -0
  14. package/gsd-core/bin/lib/command-roster.cjs +19 -0
  15. package/gsd-core/bin/lib/commands.cjs +33 -10
  16. package/gsd-core/bin/lib/config-loader.cjs +7 -8
  17. package/gsd-core/bin/lib/config-schema.cjs +32 -3
  18. package/gsd-core/bin/lib/config.cjs +1 -2
  19. package/gsd-core/bin/lib/core.cjs +5 -2
  20. package/gsd-core/bin/lib/init.cjs +8 -5
  21. package/gsd-core/bin/lib/install-profiles.cjs +2 -1
  22. package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
  23. package/gsd-core/bin/lib/model-resolver.cjs +47 -5
  24. package/gsd-core/bin/lib/phase.cjs +71 -11
  25. package/gsd-core/bin/lib/profile-output.cjs +45 -4
  26. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
  27. package/gsd-core/bin/lib/roadmap.cjs +97 -7
  28. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1700 -0
  29. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +20 -28
  30. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
  31. package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
  32. package/gsd-core/bin/lib/state-command-router.cjs +15 -3
  33. package/gsd-core/bin/lib/state-document.cjs +46 -1
  34. package/gsd-core/bin/lib/state.cjs +221 -35
  35. package/gsd-core/bin/lib/verify.cjs +19 -4
  36. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
  37. package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
  38. package/gsd-core/bin/shared/model-catalog.json +1 -0
  39. package/gsd-core/references/loop-hook-dispatch.md +61 -0
  40. package/gsd-core/templates/config.json +1 -1
  41. package/gsd-core/workflows/audit-milestone.md +5 -3
  42. package/gsd-core/workflows/autonomous.md +10 -5
  43. package/gsd-core/workflows/code-review-fix.md +9 -7
  44. package/gsd-core/workflows/code-review.md +8 -6
  45. package/gsd-core/workflows/discuss-phase.md +14 -0
  46. package/gsd-core/workflows/execute-phase.md +82 -143
  47. package/gsd-core/workflows/help/modes/full.md +3 -1
  48. package/gsd-core/workflows/new-project.md +3 -3
  49. package/gsd-core/workflows/plan-phase.md +89 -221
  50. package/gsd-core/workflows/profile-user.md +3 -1
  51. package/gsd-core/workflows/progress.md +58 -1
  52. package/gsd-core/workflows/quick.md +9 -6
  53. package/gsd-core/workflows/review.md +19 -2
  54. package/gsd-core/workflows/secure-phase.md +4 -2
  55. package/gsd-core/workflows/settings-advanced.md +2 -0
  56. package/gsd-core/workflows/ship.md +57 -0
  57. package/gsd-core/workflows/validate-phase.md +4 -2
  58. package/gsd-core/workflows/verify-work.md +27 -11
  59. package/package.json +3 -2
  60. package/scripts/affected-tests-lib.cjs +16 -4
  61. package/scripts/gen-capability-registry.cjs +359 -48
  62. package/scripts/gen-inventory-manifest.cjs +1 -4
  63. package/scripts/gen-loop-host-contract.cjs +55 -0
  64. package/scripts/issue-version-gate.cjs +140 -0
  65. package/scripts/lint-allow-test-rule-refs.allowlist.json +326 -0
  66. package/scripts/lint-allow-test-rule-refs.cjs +162 -0
  67. package/scripts/lint-test-file-count.allowlist.json +8 -0
  68. package/scripts/release-tarball-smoke.cjs +7 -1
  69. package/scripts/run-tests.cjs +74 -14
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gsd-core",
3
3
  "displayName": "GSD Core",
4
- "version": "1.5.0-rc.2",
4
+ "version": "1.5.0-rc.3",
5
5
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
6
6
  "author": {
7
7
  "name": "open-gsd",
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: gsd-mempalace-curator
3
+ description: Ship-time MemPalace curation — writes the session diary, proposes/creates cross-project tunnels, mirrors extract-learnings into the temporal KG, and runs wing-scoped drawer pruning. Spawned at ship:post by the mempalace capability.
4
+ tools: Read, Bash, Grep, Glob
5
+ model: sonnet
6
+ color: cyan
7
+ ---
8
+
9
+ <role>
10
+ You are the MemPalace curator. You run once per phase at `ship:post`, after verification has passed, to consolidate the phase's memory into the palace. Everything you do is best-effort and wing-scoped: a MemPalace failure must never fail the ship step (`onError: skip`), and you must never touch drawers outside this project's wing.
11
+ </role>
12
+
13
+ <inputs>
14
+ - `.planning/config.json` — read `mempalace.enabled`, `mempalace.memory_mode`, `mempalace.wing`, `mempalace.diary_journal`, `mempalace.cross_project_tunnels`, `mempalace.mirror_kg`, `project_code`.
15
+ - The completed phase artifacts: `UAT.md`, `SUMMARY.md`, and any `extract-learnings` output.
16
+ </inputs>
17
+
18
+ ## Gate
19
+
20
+ If `mempalace.enabled !== true`, do nothing and report `MemPalace disabled — curation skipped`. This is the hard gate; respect it before any other work.
21
+
22
+ ## Wing / mode / transport
23
+
24
+ - **Wing:** `mempalace.wing` if non-empty, else `project_code`, else the repo directory name. Every call you make is scoped to this one wing.
25
+ - **Mode:** only `augment` is currently wired — KG writes are an additive mirror of `.planning/graphs/`. `kg_backend`/`replace` are forward-declared and behave as `augment` today.
26
+ - **Transport:** prefer the `mempalace_*` MCP tools interactively; fall back to the `mempalace` CLI in headless/cron runs. If neither is reachable, report unavailability and stop — do not error.
27
+
28
+ ## Tasks (each independently best-effort)
29
+
30
+ 1. **Diary entry** (when `mempalace.diary_journal` is true). Write one concise per-agent diary entry summarising the phase outcome: `mempalace_diary_write(agent_name=<project>/<role>, entry=<summary>, topic="phase-ship", wing=<wing>)` (CLI: `mempalace hook run` / the diary CLI). Namespace `agent_name` by repo+role so diaries don't collide across projects. **Idempotency:** before writing, `mempalace_diary_read` (or list) for an existing entry keyed by `(wing, agent_name, topic, phase-id)`; if one exists for this phase, update it in place rather than appending a second.
31
+
32
+ 2. **extract-learnings → KG mirror** (when `mempalace.mirror_kg` is true). For each decision/lesson/pattern/surprise from the phase's learnings, add a typed KG triple with provenance (`source_file`, `source_drawer_id`) and `valid_from` = the phase date. **Idempotency:** the triple `(subject, predicate, object)` is the natural key — `mempalace_kg_query` for it first and skip `mempalace_kg_add` if it already exists with the same `valid_from`, so reruns don't fork duplicate facts. When a prior decision was superseded this phase, call `mempalace_kg_invalidate` to set its `valid_to` rather than deleting it.
33
+
34
+ 3. **Cross-project tunnels** (when `mempalace.cross_project_tunnels` is true). Use `mempalace_find_tunnels` to surface related wings, then `mempalace_create_tunnel(label=…)` only for connections you (or the user) can justify. **Idempotency:** check the `find_tunnels` result first and skip creation if a tunnel with that `(source-wing, target-wing, label)` already exists. Do not mass-create tunnels.
35
+
36
+ 4. **Wing-scoped prune** (optional). Run `mempalace sync --wing <wing> --apply` to prune drawers whose source artifacts were archived/deleted. **Never** run a global sync/prune; always pass `--wing`.
37
+
38
+ ## Hard rules
39
+
40
+ - Best-effort only: catch and report every MemPalace failure; never propagate an error that would fail `ship:post`.
41
+ - Wing-scoped only: never read, write, or prune outside this project's wing.
42
+ - Verbatim preservation: invalidate superseded facts (set `valid_to`); do not destroy history.
43
+ - Idempotent: re-running a shipped phase must not duplicate diary entries, facts, or tunnels.
44
+
45
+ ## Report
46
+
47
+ Emit a short summary of what was curated: diary (yes/no), KG facts mirrored (count), tunnels proposed/created (count), drawers pruned (count) — or `MemPalace unavailable — curation skipped`.
package/bin/install.js CHANGED
@@ -26,8 +26,8 @@ const {
26
26
  // fs.readdirSync + RegExp work for every skill.
27
27
  const {
28
28
  transformContentToHyphen,
29
- readCmdNames: readGsdCommandNames,
30
- } = require(path.join(__dirname, '..', 'scripts', 'fix-slash-commands.cjs'));
29
+ readGsdCommandNames,
30
+ } = require('../gsd-core/bin/lib/command-roster.cjs');
31
31
  const {
32
32
  resolveAntigravityGlobalDir,
33
33
  getGlobalConfigDir,
@@ -38,6 +38,7 @@ const {
38
38
  readBaseRefFromSettings,
39
39
  } = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
40
40
  const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
41
+ const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
41
42
  // Canonical set of hook files shipped to users. Imported here so writeManifest()
42
43
  // records exactly the same set that build-hooks.js copies to hooks/dist/, making
43
44
  // the manifest and the installed hooks/ dir structurally identical. Avoids the
@@ -3166,7 +3167,7 @@ purpose: ${toSingleLine(description)}
3166
3167
  * @param {object|null} runtimeResolver — runtime-aware tier resolver from readGsdRuntimeProfileResolver
3167
3168
  * @param {object|null} effortCfg — #443: merged effort config from readGsdEffectiveEffortConfig
3168
3169
  */
3169
- function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, runtimeResolver = null, effortCfg = null) {
3170
+ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, runtimeResolver = null, effortCfg = null, sandboxTier = 'codex-agent-sandbox') {
3170
3171
  const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
3171
3172
  const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
3172
3173
  const frontmatterText = frontmatter || '';
@@ -3179,8 +3180,10 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
3179
3180
  const lines = [
3180
3181
  `name = ${JSON.stringify(resolvedName)}`,
3181
3182
  `description = ${JSON.stringify(resolvedDescription)}`,
3182
- `sandbox_mode = "${sandboxMode}"`,
3183
3183
  ];
3184
+ if (sandboxTier != null && sandboxTier !== 'none') {
3185
+ lines.push(`sandbox_mode = "${sandboxMode}"`);
3186
+ }
3184
3187
 
3185
3188
  // Embed model override when configured in ~/.gsd/defaults.json so that
3186
3189
  // model_overrides is respected on Codex (which uses static TOML, not inline
@@ -5745,7 +5748,7 @@ function writeCopilotHookConfig(targetDir) {
5745
5748
  * Generate config.toml and per-agent .toml files for Codex.
5746
5749
  * Reads agent .md files from source, extracts metadata, writes .toml configs.
5747
5750
  */
5748
- function installCodexConfig(targetDir, agentsSrc) {
5751
+ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-sandbox') {
5749
5752
  const configPath = path.join(targetDir, 'config.toml');
5750
5753
  const agentsTomlDir = path.join(targetDir, 'agents');
5751
5754
  fs.mkdirSync(agentsTomlDir, { recursive: true });
@@ -5791,7 +5794,7 @@ function installCodexConfig(targetDir, agentsSrc) {
5791
5794
  // #443 — pass unified effort config so model_reasoning_effort in the .toml
5792
5795
  // follows the same config-driven precedence as the Claude .md effort key.
5793
5796
  const effortCfg = readGsdEffectiveEffortConfig(targetDir);
5794
- const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver, effortCfg);
5797
+ const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver, effortCfg, sandboxTier);
5795
5798
  fs.writeFileSync(path.join(agentsTomlDir, `${name}.toml`), tomlContent);
5796
5799
  }
5797
5800
 
@@ -6924,7 +6927,13 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6924
6927
  content = content.replace(/~\/\.claude\//g, pathPrefix);
6925
6928
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6926
6929
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6930
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
6931
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
6932
+ content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
6927
6933
  content = content.replace(/~\/\.augment\//g, pathPrefix);
6934
+ content = content.replace(/\$HOME\/\.augment\//g, pathPrefix);
6935
+ content = content.replace(/~\/\.augment(?![\w-])/g, normalizedPathPrefix);
6936
+ content = content.replace(/\$HOME\/\.augment(?![\w-])/g, normalizedPathPrefix);
6928
6937
  content = processAttribution(content, getCommitAttribution(runtime));
6929
6938
  break;
6930
6939
 
@@ -6984,6 +6993,10 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6984
6993
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6985
6994
  content = content.replace(/~\/\.qwen\//g, pathPrefix);
6986
6995
  content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
6996
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
6997
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
6998
+ content = content.replace(/~\/\.qwen(?![\w-])/g, normalizedPathPrefix);
6999
+ content = content.replace(/\$HOME\/\.qwen(?![\w-])/g, normalizedPathPrefix);
6987
7000
  // Bare relative .claude/ → .qwen/ (residual refs not matched above)
6988
7001
  content = content.replace(/\.claude\//g, '.qwen/');
6989
7002
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
@@ -7000,6 +7013,10 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
7000
7013
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
7001
7014
  content = content.replace(/~\/\.hermes\//g, pathPrefix);
7002
7015
  content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
7016
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
7017
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
7018
+ content = content.replace(/~\/\.hermes(?![\w-])/g, normalizedPathPrefix);
7019
+ content = content.replace(/\$HOME\/\.hermes(?![\w-])/g, normalizedPathPrefix);
7003
7020
  // Bare relative .claude/ → .hermes/ (residual refs)
7004
7021
  content = content.replace(/\.claude\//g, '.hermes/');
7005
7022
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
@@ -10717,7 +10734,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10717
10734
  if (!isMinimalMode(_effectiveInstallMode)) {
10718
10735
  try {
10719
10736
  // Generate Codex config.toml and per-agent .toml files.
10720
- agentCount = installCodexConfig(targetDir, agentsSrc);
10737
+ agentCount = installCodexConfig(targetDir, agentsSrc, plan.sandboxTier);
10721
10738
  } catch (e) {
10722
10739
  restoreCodexSnapshot();
10723
10740
  throw e;
@@ -12265,6 +12282,7 @@ module.exports = {
12265
12282
  parseConfigDirFromArgs,
12266
12283
  cleanupLegacyGsdCc,
12267
12284
  _applyRuntimeRewrites,
12285
+ ...runtimeArtifactConversion,
12268
12286
  };
12269
12287
 
12270
12288
  // Main logic — only run when not loaded as a module for testing
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: gsd:mempalace-capture
3
+ description: "File a phase artifact into MemPalace; mirror decision facts into its temporal KG"
4
+ argument-hint: "[CONTEXT.md|PLAN.md|SUMMARY.md]"
5
+ allowed-tools:
6
+ - Read
7
+ - Bash
8
+ requires: [config]
9
+ ---
10
+
11
+ **STOP -- DO NOT READ THIS FILE. You are already reading it. This prompt was injected into your context by the command system. Using the Read tool on this file wastes tokens. Begin executing Step 0 immediately.**
12
+
13
+ ## Step 0 -- Banner
14
+
15
+ **Before ANY tool calls**, display this banner:
16
+
17
+ ```
18
+ GSD > MEMPALACE CAPTURE
19
+ ```
20
+
21
+ Then proceed to Step 1.
22
+
23
+ ## Step 1 -- Config Gate
24
+
25
+ Check whether the MemPalace capability is enabled by reading `.planning/config.json` directly with the Read tool.
26
+
27
+ 1. Read `.planning/config.json` with the Read tool.
28
+ 2. If the file does not exist, or `config.mempalace` is absent, or `config.mempalace.enabled !== true`, or `config.mempalace.capture_artifacts !== true`: display the disabled message and **STOP**.
29
+ 3. Otherwise proceed to Step 2.
30
+
31
+ **Disabled message:**
32
+
33
+ ```
34
+ GSD > MEMPALACE CAPTURE
35
+
36
+ MemPalace capture is disabled (mempalace.enabled / mempalace.capture_artifacts).
37
+ Nothing was filed; the loop proceeds normally.
38
+ ```
39
+
40
+ This step is `onError: skip` at `discuss:post` / `plan:post` / `verify:post` -- capture never fails a phase.
41
+
42
+ ## Step 2 -- Resolve target
43
+
44
+ 1. **Artifact.** Take the artifact from `$ARGUMENTS`. If absent, infer from the loop point: `discuss:post` → `CONTEXT.md`, `plan:post` → `PLAN.md`, `verify:post` → `SUMMARY.md`.
45
+ 2. **Room.** Map artifact → room:
46
+ - `CONTEXT.md` → `decisions`
47
+ - `PLAN.md` → `planning`
48
+ - `SUMMARY.md` → `milestones`
49
+ (Confirmed problem→fix pairs go to `problems` — see the `capture-problems` fragment used at `execute:wave:post`.)
50
+ 3. **Wing.** `config.mempalace.wing` if non-empty, else `config.project_code`, else the repo directory name.
51
+ 4. **Mode / transport.** Read `config.mempalace.memory_mode`. Prefer MCP (`mempalace_*`) when your MemPalace MCP server is registered and your runtime permits those tools; otherwise use the `mempalace` CLI (covered by this skill's `Bash` allow-tool), as in `mempalace-recall`.
52
+
53
+ ## Step 3 -- File verbatim (idempotent)
54
+
55
+ On any error or timeout, stop and let the phase continue -- capture is best-effort.
56
+
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
+ 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.)
60
+ 4. Re-running a phase MUST NOT create duplicate drawers (deterministic ids + `check_duplicate`).
61
+
62
+ ## Step 4 -- Report
63
+
64
+ Print a one-line summary: `Filed <artifact> → <wing>/<room> (<n> KG facts)` or `MemPalace unavailable — capture skipped`.
65
+
66
+ ## Anti-Patterns
67
+
68
+ 1. DO NOT let any MemPalace error fail the step -- capture is `onError: skip`.
69
+ 2. DO NOT write lossy summaries -- store the verbatim artifact text (AAAK compression is a separate, optional index).
70
+ 3. DO NOT prune or delete drawers here -- pruning (`sync --apply`) is the curator agent's job at `ship:post`, wing-scoped only.
71
+ 4. DO NOT skip the config gate or the dedup check.
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: gsd:mempalace-recall
3
+ description: "Recall decisions, patterns, and surprises from MemPalace before planning"
4
+ argument-hint: "[phase-slug]"
5
+ allowed-tools:
6
+ - Read
7
+ - Write
8
+ - Bash
9
+ requires: [config]
10
+ ---
11
+
12
+ **STOP -- DO NOT READ THIS FILE. You are already reading it. This prompt was injected into your context by the command system. Using the Read tool on this file wastes tokens. Begin executing Step 0 immediately.**
13
+
14
+ ## Step 0 -- Banner
15
+
16
+ **Before ANY tool calls**, display this banner:
17
+
18
+ ```
19
+ GSD > MEMPALACE RECALL
20
+ ```
21
+
22
+ Then proceed to Step 1.
23
+
24
+ ## Step 1 -- Config Gate
25
+
26
+ Check whether the MemPalace capability is enabled by reading `.planning/config.json` directly with the Read tool.
27
+
28
+ **DO NOT use `gsd-tools config get-value`** -- it hard-exits on missing keys.
29
+
30
+ 1. Read `.planning/config.json` with the Read tool.
31
+ 2. If the file does not exist: write the "unavailable" stub (Step 4) and **STOP**.
32
+ 3. Parse the JSON. Proceed to Step 2 only if `config.mempalace && config.mempalace.enabled === true` **and** `config.mempalace.recall_on_plan !== false`. Otherwise display the disabled message and **STOP** (`recall_on_plan: false` turns plan-time recall off while leaving the rest of the capability enabled).
33
+
34
+ **Disabled message:**
35
+
36
+ ```
37
+ GSD > MEMPALACE RECALL
38
+
39
+ MemPalace memory is disabled. To activate:
40
+
41
+ node <runtime-home>/gsd-core/bin/gsd-tools.cjs config-set mempalace.enabled true
42
+
43
+ Recall is opt-in; the loop proceeds normally without it.
44
+ ```
45
+
46
+ This step is `onError: skip` at `plan:pre` -- recall never blocks planning.
47
+
48
+ ## Step 2 -- Resolve wing, mode, and transport
49
+
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`.
52
+ 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
+ 4. **Topic.** Read the phase `CONTEXT.md` (the consumed artifact). Derive a short search query from its title, goal, and key decisions.
54
+
55
+ ## Step 3 -- Retrieve (read-only)
56
+
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.
58
+
59
+ 1. **Wake up** (cheap, ~600--900 tokens):
60
+ - Interactive: read the wing identity/summary, then `mempalace_search`.
61
+ - Headless: `mempalace wake-up --wing <wing>`.
62
+ 2. **Targeted search:**
63
+ - Interactive: `mempalace_search(query=<topic>, wing=<wing>)`.
64
+ - 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.)
66
+ 4. **Dedup** the returned drawers/facts; keep the top results.
67
+
68
+ ## Step 4 -- Write MEMORY-RECALL.md
69
+
70
+ Write `MEMORY-RECALL.md` in the current phase directory. The planner consumes it.
71
+
72
+ When recall succeeded, structure it as:
73
+
74
+ ```markdown
75
+ # Memory Recall (MemPalace)
76
+
77
+ _Wing: <wing> · Mode: <mode> · Transport: <mcp|cli>_
78
+
79
+ ## Prior decisions
80
+ - <decision> — <provenance: drawer id / kg fact, valid_from>
81
+
82
+ ## Patterns
83
+ - <pattern> — <provenance>
84
+
85
+ ## Surprises / gotchas
86
+ - <surprise> — <provenance>
87
+ ```
88
+
89
+ When MemPalace is unreachable, write the stub and continue:
90
+
91
+ ```markdown
92
+ # Memory Recall (MemPalace)
93
+
94
+ _MemPalace unavailable at recall time — proceeding without recalled memory._
95
+ ```
96
+
97
+ ## Anti-Patterns
98
+
99
+ 1. DO NOT let any MemPalace error fail the step -- recall is `onError: skip`.
100
+ 2. DO NOT write to the palace from this skill -- recall is read-only; capture is a separate skill.
101
+ 3. DO NOT paste raw search output into the file -- distil to decisions/patterns/surprises with provenance.
102
+ 4. DO NOT skip the config gate.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: gsd-context
3
- description: "codebase intelligence | map graphify docs learnings"
3
+ description: "codebase intel | map graphify docs learnings mempalace"
4
4
  argument-hint: ""
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Skill
8
- requires: [map-codebase, graphify, docs-update, extract-learnings]
8
+ requires: [map-codebase, graphify, docs-update, extract-learnings, mempalace-recall, mempalace-capture]
9
9
  ---
10
10
 
11
11
  Route to the appropriate codebase-intelligence skill based on the user's intent.
@@ -19,5 +19,7 @@ Route to the appropriate codebase-intelligence skill based on the user's intent.
19
19
  | Generate a knowledge graph | gsd-graphify |
20
20
  | Update project documentation | gsd-docs-update |
21
21
  | Extract learnings from a completed phase | gsd-extract-learnings |
22
+ | Recall prior decisions and patterns before planning | gsd-mempalace-recall |
23
+ | File a phase artifact into MemPalace | gsd-mempalace-capture |
22
24
 
23
25
  Invoke the matched skill directly using the Skill tool.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.5.0-rc.2",
3
+ "version": "1.5.0-rc.3",
4
4
  "description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -58,6 +58,11 @@
58
58
  * [--name <name>]
59
59
  * [--archive-phases] Move phase dirs to milestones/vX.Y-phases/
60
60
  *
61
+ * User Story Validation:
62
+ * user-story validate --story "..." Validate "As a / I want to / so that" format
63
+ * Returns JSON { valid, errors[], slots: {role,capability,outcome} | null }
64
+ * --pick valid Emit bare boolean (for workflow boolean checks)
65
+ *
61
66
  * Validation:
62
67
  * validate consistency Check phase numbering, disk/roadmap sync
63
68
  * validate health [--repair] Check .planning/ integrity, optionally repair
@@ -201,8 +206,6 @@ const milestone = require('./lib/milestone.cjs');
201
206
  const commands = require('./lib/commands.cjs');
202
207
  const init = require('./lib/init.cjs');
203
208
  const frontmatter = require('./lib/frontmatter.cjs');
204
- const profilePipeline = require('./lib/profile-pipeline.cjs');
205
- const profileOutput = require('./lib/profile-output.cjs');
206
209
  const workstream = require('./lib/workstream.cjs');
207
210
  const docs = require('./lib/docs.cjs');
208
211
  const learnings = require('./lib/learnings.cjs');
@@ -510,7 +513,7 @@ async function main() {
510
513
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
511
514
  'capability, classify-confidence, learnings, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
512
515
  'profile-sample, progress, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
513
- 'task, template, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
516
+ 'task, template, user-story, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
514
517
  'Global flags:\n' +
515
518
  ' --raw Emit raw output without post-processing\n' +
516
519
  ' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
@@ -559,6 +562,7 @@ async function main() {
559
562
  'verify-summary', 'template', 'frontmatter', 'detect-custom-files',
560
563
  'worktree', 'prompt-budget',
561
564
  'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
565
+ 'user-story', // pure string validation — no .planning/ access needed
562
566
  ]);
563
567
  if (!SKIP_ROOT_RESOLUTION.has(command)) {
564
568
  cwd = findProjectRoot(cwd);
@@ -1236,7 +1240,39 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1236
1240
  // loop render-hooks <point>
1237
1241
  const loopSubcommand = args[1];
1238
1242
  if (loopSubcommand === 'render-hooks') {
1239
- loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {});
1243
+ let loopConfigDir = null;
1244
+ const configDirEqArg = args.find(arg => arg.startsWith('--config-dir='));
1245
+ const configDirIdx = args.indexOf('--config-dir');
1246
+ if (configDirEqArg) {
1247
+ const value = configDirEqArg.slice('--config-dir='.length).trim();
1248
+ if (!value) error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1249
+ loopConfigDir = value;
1250
+ } else if (configDirIdx !== -1) {
1251
+ const value = args[configDirIdx + 1];
1252
+ if (!value || value.startsWith('--')) {
1253
+ error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1254
+ }
1255
+ loopConfigDir = value;
1256
+ }
1257
+ // --active-cap <capId>: parse and validate before delegating
1258
+ let loopActiveCap = undefined;
1259
+ const activeCapEqArg = args.find(arg => arg.startsWith('--active-cap='));
1260
+ const activeCapIdx = args.indexOf('--active-cap');
1261
+ if (activeCapEqArg) {
1262
+ const value = activeCapEqArg.slice('--active-cap='.length).trim();
1263
+ if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1264
+ loopActiveCap = value;
1265
+ } else if (activeCapIdx !== -1) {
1266
+ const value = args[activeCapIdx + 1];
1267
+ if (!value || value.startsWith('--')) {
1268
+ error('Missing value for --active-cap (e.g. --active-cap tdd)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1269
+ }
1270
+ loopActiveCap = value;
1271
+ }
1272
+ loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {
1273
+ configDir: loopConfigDir ? path.resolve(loopConfigDir) : undefined,
1274
+ activeCap: loopActiveCap,
1275
+ });
1240
1276
  } else {
1241
1277
  error(
1242
1278
  `Unknown loop subcommand: ${loopSubcommand}. Available: render-hooks`,
@@ -1305,94 +1341,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1305
1341
  break;
1306
1342
  }
1307
1343
 
1308
- // ─── Profiling Pipeline ────────────────────────────────────────────────
1309
-
1310
- case 'scan-sessions': {
1311
- const pathIdx = args.indexOf('--path');
1312
- const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
1313
- const verboseFlag = args.includes('--verbose');
1314
- const jsonFlag = args.includes('--json');
1315
- await profilePipeline.cmdScanSessions(sessionsPath, { verbose: verboseFlag, json: jsonFlag }, raw);
1316
- break;
1317
- }
1318
-
1319
- case 'extract-messages': {
1320
- const sessionIdx = args.indexOf('--session');
1321
- const sessionId = sessionIdx !== -1 ? args[sessionIdx + 1] : null;
1322
- const limitIdx = args.indexOf('--limit');
1323
- const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : null;
1324
- const pathIdx = args.indexOf('--path');
1325
- const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
1326
- const projectArg = args[1];
1327
- if (!projectArg || projectArg.startsWith('--')) {
1328
- error('Usage: gsd-tools extract-messages <project> [--session <id>] [--limit N] [--path <dir>]\nRun scan-sessions first to see available projects.', ERROR_REASON.USAGE);
1329
- }
1330
- await profilePipeline.cmdExtractMessages(projectArg, { sessionId, limit }, raw, sessionsPath);
1331
- break;
1332
- }
1333
-
1334
- case 'profile-sample': {
1335
- const pathIdx = args.indexOf('--path');
1336
- const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
1337
- const limitIdx = args.indexOf('--limit');
1338
- const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 150;
1339
- const maxPerIdx = args.indexOf('--max-per-project');
1340
- const maxPerProject = maxPerIdx !== -1 ? parseInt(args[maxPerIdx + 1], 10) : null;
1341
- const maxCharsIdx = args.indexOf('--max-chars');
1342
- const maxChars = maxCharsIdx !== -1 ? parseInt(args[maxCharsIdx + 1], 10) : 500;
1343
- await profilePipeline.cmdProfileSample(sessionsPath, { limit, maxPerProject, maxChars }, raw);
1344
- break;
1345
- }
1346
-
1347
- // ─── Profile Output ──────────────────────────────────────────────────
1348
-
1349
- case 'write-profile': {
1350
- const inputIdx = args.indexOf('--input');
1351
- const inputPath = inputIdx !== -1 ? args[inputIdx + 1] : null;
1352
- if (!inputPath) error('--input <analysis-json-path> is required', ERROR_REASON.USAGE);
1353
- const outputIdx = args.indexOf('--output');
1354
- const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
1355
- profileOutput.cmdWriteProfile(cwd, { input: inputPath, output: outputPath }, raw);
1356
- break;
1357
- }
1358
-
1359
- case 'profile-questionnaire': {
1360
- const answersIdx = args.indexOf('--answers');
1361
- const answers = answersIdx !== -1 ? args[answersIdx + 1] : null;
1362
- profileOutput.cmdProfileQuestionnaire({ answers }, raw);
1363
- break;
1364
- }
1365
-
1366
- case 'generate-dev-preferences': {
1367
- const analysisIdx = args.indexOf('--analysis');
1368
- const analysisPath = analysisIdx !== -1 ? args[analysisIdx + 1] : null;
1369
- const outputIdx = args.indexOf('--output');
1370
- const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
1371
- const stackIdx = args.indexOf('--stack');
1372
- const stack = stackIdx !== -1 ? args[stackIdx + 1] : null;
1373
- profileOutput.cmdGenerateDevPreferences(cwd, { analysis: analysisPath, output: outputPath, stack }, raw);
1374
- break;
1375
- }
1376
-
1377
- case 'generate-claude-profile': {
1378
- const analysisIdx = args.indexOf('--analysis');
1379
- const analysisPath = analysisIdx !== -1 ? args[analysisIdx + 1] : null;
1380
- const outputIdx = args.indexOf('--output');
1381
- const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
1382
- const globalFlag = args.includes('--global');
1383
- profileOutput.cmdGenerateClaudeProfile(cwd, { analysis: analysisPath, output: outputPath, global: globalFlag }, raw);
1384
- break;
1385
- }
1386
-
1387
- case 'generate-claude-md': {
1388
- const outputIdx = args.indexOf('--output');
1389
- const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
1390
- const autoFlag = args.includes('--auto');
1391
- const forceFlag = args.includes('--force');
1392
- profileOutput.cmdGenerateClaudeMd(cwd, { output: outputPath, auto: autoFlag, force: forceFlag }, raw);
1393
- break;
1394
- }
1395
-
1396
1344
  case 'workstream': {
1397
1345
  const subcommand = args[1];
1398
1346
  if (subcommand === 'create') {
@@ -1984,6 +1932,76 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1984
1932
  break;
1985
1933
  }
1986
1934
 
1935
+ // ─── User Story Validation (bug #1145) ────────────────────────────────────
1936
+ //
1937
+ // Invocation shapes (from mvp-phase.md and verify-work.md):
1938
+ // gsd_run query user-story.validate --story "$USER_STORY"
1939
+ // gsd_run query user-story.validate --story "$PHASE_GOAL" --pick valid
1940
+ //
1941
+ // Returns JSON: { valid: boolean, errors: string[], slots: { role, capability, outcome } | null }
1942
+ // - valid: true only when the story fully matches the canonical format
1943
+ // - errors: per-slot diagnostic strings (empty on success)
1944
+ // - slots: extracted role/capability/outcome on success; null on failure
1945
+ //
1946
+ // Canonical format (user-story-template.md):
1947
+ // "As a [user role], I want to [capability], so that [outcome]."
1948
+ // Each slot must be non-empty and contain non-whitespace content.
1949
+ //
1950
+ // No .planning/ access needed — pure string validation.
1951
+ case 'user-story': {
1952
+ const subcommand = args[1];
1953
+ if (subcommand !== 'validate') {
1954
+ error(`Unknown user-story subcommand: ${subcommand || '(none)'}. Available: validate`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
1955
+ break;
1956
+ }
1957
+
1958
+ const storyIdx = args.indexOf('--story');
1959
+ const story = (storyIdx !== -1 && args[storyIdx + 1] && !args[storyIdx + 1].startsWith('--'))
1960
+ ? args[storyIdx + 1]
1961
+ : '';
1962
+
1963
+ // Canonical extraction regex — requires non-whitespace content in each slot
1964
+ // (\S.*? ensures the slot isn't whitespace-only).
1965
+ // Named groups: role / capability / outcome.
1966
+ const USER_STORY_RE = /^As a (\S.*?), I want to (\S.*?), so that (\S.*?)\.$/;
1967
+
1968
+ const errors = [];
1969
+ const trimmed = story.trim();
1970
+ let slots = null;
1971
+
1972
+ if (!trimmed) {
1973
+ errors.push('Story is empty. Required format: "As a [role], I want to [capability], so that [outcome]."');
1974
+ } else {
1975
+ // Per-clause guards produce targeted, actionable error messages before
1976
+ // attempting the full regex. Guards are ordered: role → capability → outcome → period.
1977
+ if (!/^As a \S/i.test(trimmed)) {
1978
+ errors.push('Story must start with "As a [user role]," (role must be non-empty).');
1979
+ }
1980
+ if (!/, I want to \S/i.test(trimmed)) {
1981
+ errors.push('Story must include ", I want to [capability]," (capability must be non-empty).');
1982
+ }
1983
+ if (!/, so that \S/i.test(trimmed)) {
1984
+ errors.push('Story must include ", so that [outcome]." (outcome must be non-empty).');
1985
+ }
1986
+ if (!trimmed.endsWith('.')) {
1987
+ errors.push('Story must end with a period (.).');
1988
+ }
1989
+ // Full-regex check only when per-clause guards all passed — avoids
1990
+ // redundant "format mismatch" noise on top of specific error messages.
1991
+ if (errors.length === 0) {
1992
+ const m = USER_STORY_RE.exec(trimmed);
1993
+ if (!m) {
1994
+ errors.push('Story does not match the canonical format: "As a [role], I want to [capability], so that [outcome]."');
1995
+ } else {
1996
+ slots = { role: m[1], capability: m[2], outcome: m[3] };
1997
+ }
1998
+ }
1999
+ }
2000
+
2001
+ core.output({ valid: errors.length === 0, errors, slots }, raw);
2002
+ break;
2003
+ }
2004
+
1987
2005
  default: {
1988
2006
  // ADR-959: try capability-registry dispatch before emitting the unknown-command error.
1989
2007
  // An unmigrated command still hits its hardcoded `case` above — untouched.