@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.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-mempalace-curator.md +47 -0
- package/bin/install.js +25 -7
- package/commands/gsd/mempalace-capture.md +71 -0
- package/commands/gsd/mempalace-recall.md +102 -0
- package/commands/gsd/ns-context.md +4 -2
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +110 -92
- package/gsd-core/bin/lib/capability-activation.cjs +86 -0
- package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
- package/gsd-core/bin/lib/capability-state.cjs +31 -18
- package/gsd-core/bin/lib/check-command-router.cjs +328 -1
- package/gsd-core/bin/lib/clusters.cjs +2 -0
- package/gsd-core/bin/lib/command-roster.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +33 -10
- package/gsd-core/bin/lib/config-loader.cjs +7 -8
- package/gsd-core/bin/lib/config-schema.cjs +32 -3
- package/gsd-core/bin/lib/config.cjs +1 -2
- package/gsd-core/bin/lib/core.cjs +5 -2
- package/gsd-core/bin/lib/init.cjs +8 -5
- package/gsd-core/bin/lib/install-profiles.cjs +2 -1
- package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
- package/gsd-core/bin/lib/model-resolver.cjs +47 -5
- package/gsd-core/bin/lib/phase.cjs +71 -11
- package/gsd-core/bin/lib/profile-output.cjs +45 -4
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
- package/gsd-core/bin/lib/roadmap.cjs +97 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1700 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +20 -28
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
- package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
- package/gsd-core/bin/lib/state-command-router.cjs +15 -3
- package/gsd-core/bin/lib/state-document.cjs +46 -1
- package/gsd-core/bin/lib/state.cjs +221 -35
- package/gsd-core/bin/lib/verify.cjs +19 -4
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
- package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
- package/gsd-core/bin/shared/model-catalog.json +1 -0
- package/gsd-core/references/loop-hook-dispatch.md +61 -0
- package/gsd-core/templates/config.json +1 -1
- package/gsd-core/workflows/audit-milestone.md +5 -3
- package/gsd-core/workflows/autonomous.md +10 -5
- package/gsd-core/workflows/code-review-fix.md +9 -7
- package/gsd-core/workflows/code-review.md +8 -6
- package/gsd-core/workflows/discuss-phase.md +14 -0
- package/gsd-core/workflows/execute-phase.md +82 -143
- package/gsd-core/workflows/help/modes/full.md +3 -1
- package/gsd-core/workflows/new-project.md +3 -3
- package/gsd-core/workflows/plan-phase.md +89 -221
- package/gsd-core/workflows/profile-user.md +3 -1
- package/gsd-core/workflows/progress.md +58 -1
- package/gsd-core/workflows/quick.md +9 -6
- package/gsd-core/workflows/review.md +19 -2
- package/gsd-core/workflows/secure-phase.md +4 -2
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/ship.md +57 -0
- package/gsd-core/workflows/validate-phase.md +4 -2
- package/gsd-core/workflows/verify-work.md +27 -11
- package/package.json +3 -2
- package/scripts/affected-tests-lib.cjs +16 -4
- package/scripts/gen-capability-registry.cjs +359 -48
- package/scripts/gen-inventory-manifest.cjs +1 -4
- package/scripts/gen-loop-host-contract.cjs +55 -0
- package/scripts/issue-version-gate.cjs +140 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +326 -0
- package/scripts/lint-allow-test-rule-refs.cjs +162 -0
- package/scripts/lint-test-file-count.allowlist.json +8 -0
- package/scripts/release-tarball-smoke.cjs +7 -1
- 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.
|
|
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
|
-
|
|
30
|
-
} = require(
|
|
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
|
|
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.
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-core",
|
|
3
|
-
"version": "1.5.0-rc.
|
|
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
|
-
|
|
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.
|