@opengsd/gsd-core 1.5.0-rc.5 → 1.5.0
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/LICENSE +1 -1
- package/agents/gsd-executor.md +22 -0
- package/bin/install.js +53 -99
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +36 -5
- package/gsd-core/bin/lib/decisions.cjs +19 -1
- package/gsd-core/bin/lib/phase-id.cjs +1 -1
- package/gsd-core/bin/lib/phase.cjs +41 -6
- package/gsd-core/bin/lib/probe-core.cjs +7 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +201 -26
- package/gsd-core/bin/lib/roadmap-parser.cjs +25 -20
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +4 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +24 -3
- package/gsd-core/bin/lib/state.cjs +64 -15
- package/gsd-core/bin/lib/teams-status.cjs +74 -0
- package/gsd-core/bin/lib/uat.cjs +56 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +7 -1
- package/gsd-core/references/prohibition-probe.md +48 -19
- package/gsd-core/references/worktree-branch-check.md +11 -5
- package/gsd-core/workflows/docs-update.md +23 -31
- package/gsd-core/workflows/execute-phase.md +2 -1
- package/gsd-core/workflows/map-codebase.md +8 -10
- package/gsd-core/workflows/plan-phase.md +6 -0
- package/gsd-core/workflows/quick.md +23 -2
- package/gsd-core/workflows/spec-phase.md +11 -6
- package/gsd-core/workflows/verify-phase.md +3 -3
- package/hooks/dist/gsd-worktree-path-guard.js +34 -18
- package/hooks/gsd-worktree-path-guard.js +34 -18
- package/package.json +2 -2
- package/scripts/ci-prepare-test-scope.cjs +56 -14
- package/scripts/diff-touches-shipped-paths.cjs +5 -11
- package/scripts/run-tests.cjs +1 -0
package/LICENSE
CHANGED
package/agents/gsd-executor.md
CHANGED
|
@@ -103,6 +103,24 @@ PLAN_START_EPOCH=$(date +%s)
|
|
|
103
103
|
```
|
|
104
104
|
</step>
|
|
105
105
|
|
|
106
|
+
<worktree_metadata_capture>
|
|
107
|
+
If running inside a git worktree, capture authoritative worktree identity before
|
|
108
|
+
any task commit changes HEAD. The execute-phase orchestrator consumes this from
|
|
109
|
+
your final `<worktree_metadata>` return block to build the wave cleanup manifest
|
|
110
|
+
without relying on runtime harness metadata (#1297).
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
GSD_WORKTREE_PATH=""
|
|
114
|
+
GSD_WORKTREE_BRANCH=""
|
|
115
|
+
GSD_WORKTREE_EXPECTED_BASE=""
|
|
116
|
+
if [ -f .git ]; then
|
|
117
|
+
GSD_WORKTREE_PATH=$(git rev-parse --show-toplevel)
|
|
118
|
+
GSD_WORKTREE_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
119
|
+
GSD_WORKTREE_EXPECTED_BASE=$(git rev-parse HEAD)
|
|
120
|
+
fi
|
|
121
|
+
```
|
|
122
|
+
</worktree_metadata_capture>
|
|
123
|
+
|
|
106
124
|
<step name="determine_execution_pattern">
|
|
107
125
|
```bash
|
|
108
126
|
grep -n "type=\"checkpoint" [plan-path]
|
|
@@ -761,6 +779,10 @@ into the user's project history.
|
|
|
761
779
|
**Tasks:** {completed}/{total}
|
|
762
780
|
**SUMMARY:** {path to SUMMARY.md}
|
|
763
781
|
|
|
782
|
+
<worktree_metadata>
|
|
783
|
+
{"agent_id":"{phase}-{plan}","worktree_path":"${GSD_WORKTREE_PATH:-}","branch":"${GSD_WORKTREE_BRANCH:-}","expected_base":"${GSD_WORKTREE_EXPECTED_BASE:-}"}
|
|
784
|
+
</worktree_metadata>
|
|
785
|
+
|
|
764
786
|
**Commits:**
|
|
765
787
|
- {hash}: {message}
|
|
766
788
|
- {hash}: {message}
|
package/bin/install.js
CHANGED
|
@@ -1796,6 +1796,10 @@ function skillFrontmatterName(skillDirName) {
|
|
|
1796
1796
|
return skillDirName;
|
|
1797
1797
|
}
|
|
1798
1798
|
|
|
1799
|
+
function normalizeClaudeSkillEffort(effort) {
|
|
1800
|
+
return effort === 'xhigh' ? 'max' : effort;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1799
1803
|
/**
|
|
1800
1804
|
* Qwen Code skills accept an optional numeric `priority` frontmatter field.
|
|
1801
1805
|
* Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified
|
|
@@ -1892,7 +1896,7 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
|
|
1892
1896
|
// token-budget tier). Fields are Claude-specific; unknown frontmatter
|
|
1893
1897
|
// fields are silently ignored by other runtimes (backward-compatible).
|
|
1894
1898
|
if (context) fm += `context: ${context}\n`;
|
|
1895
|
-
if (effort) fm += `effort: ${effort}\n`;
|
|
1899
|
+
if (effort) fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`;
|
|
1896
1900
|
if (toolsBlock) fm += toolsBlock;
|
|
1897
1901
|
fm += '---';
|
|
1898
1902
|
|
|
@@ -3194,110 +3198,55 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
|
|
3194
3198
|
}
|
|
3195
3199
|
|
|
3196
3200
|
/**
|
|
3197
|
-
*
|
|
3198
|
-
*
|
|
3199
|
-
* This file is written alongside SKILL.md as <skill-dir>/agents/openai.yaml.
|
|
3200
|
-
* Codex loads it as a SkillMetadataFile (codex-rs/core-skills/src/loader.rs),
|
|
3201
|
-
* making the skill discoverable in the /skills TUI popup with a display name
|
|
3202
|
-
* and short description. If the file is absent, Codex silently skips it (fails open).
|
|
3203
|
-
*
|
|
3204
|
-
* Schema (interface section):
|
|
3205
|
-
* display_name: short human-readable skill name (strip gsd- prefix)
|
|
3206
|
-
* short_description: 1-2 sentence description for TUI chip, ≤180 chars
|
|
3207
|
-
*
|
|
3208
|
-
* @param {string} skillName - Full skill name e.g. "gsd-plan-phase"
|
|
3209
|
-
* @param {string} shortDescription - Description text (already truncated by caller)
|
|
3210
|
-
* @returns {string} YAML content for agents/openai.yaml
|
|
3211
|
-
*/
|
|
3212
|
-
function generateCodexSkillMetadataYaml(skillName, shortDescription) {
|
|
3213
|
-
// Display name: strip "gsd-" prefix and convert hyphens to spaces for readability.
|
|
3214
|
-
const displayName = skillName.replace(/^gsd-/, '').replace(/-/g, ' ');
|
|
3215
|
-
// yamlQuote (= JSON.stringify) handles all YAML-unsafe chars: backslashes,
|
|
3216
|
-
// quotes, newlines, control characters, and Unicode escapes.
|
|
3217
|
-
return [
|
|
3218
|
-
'interface:',
|
|
3219
|
-
` display_name: ${yamlQuote(displayName)}`,
|
|
3220
|
-
` short_description: ${yamlQuote(shortDescription)}`,
|
|
3221
|
-
'',
|
|
3222
|
-
].join('\n');
|
|
3223
|
-
}
|
|
3224
|
-
|
|
3225
|
-
/**
|
|
3226
|
-
* Write agents/openai.yaml TUI chip metadata for each gsd-* skill directory.
|
|
3227
|
-
*
|
|
3228
|
-
* Called after layout-driven skill install for Codex. Iterates every gsd-*
|
|
3229
|
-
* skill directory in skillsDir, reads the SKILL.md frontmatter to extract the
|
|
3230
|
-
* short-description already emitted by convertClaudeCommandToCodexSkill, then
|
|
3231
|
-
* writes <skill-dir>/agents/openai.yaml using generateCodexSkillMetadataYaml.
|
|
3201
|
+
* Remove stale agents/openai.yaml sidecar files from GSD-managed Codex skill dirs.
|
|
3232
3202
|
*
|
|
3233
|
-
*
|
|
3234
|
-
*
|
|
3203
|
+
* Prior to #1326, GSD's Codex install path wrote an agents/openai.yaml file
|
|
3204
|
+
* alongside each gsd-* SKILL.md. Recent Codex builds index BOTH SKILL.md and
|
|
3205
|
+
* the sidecar, causing each GSD skill to appear twice in autocomplete. This
|
|
3206
|
+
* function removes those stale sidecars and — if the agents/ subdirectory is
|
|
3207
|
+
* now empty — prunes it too.
|
|
3235
3208
|
*
|
|
3236
|
-
*
|
|
3237
|
-
*
|
|
3238
|
-
*
|
|
3239
|
-
*
|
|
3240
|
-
*
|
|
3241
|
-
*
|
|
3242
|
-
*
|
|
3243
|
-
*
|
|
3244
|
-
*
|
|
3209
|
+
* Behaviour:
|
|
3210
|
+
* - Returns immediately if skillsDir does not exist (fails open).
|
|
3211
|
+
* - Only touches directories whose names start with "gsd-".
|
|
3212
|
+
* - Skips user-owned dirs (gsd-dev-preferences) — their agents/ content is
|
|
3213
|
+
* never modified, mirroring the same USER_OWNED_SKILL_DIRS guard used by
|
|
3214
|
+
* installOpencodeFamilySkills.
|
|
3215
|
+
* - For each managed gsd-* dir, if agents/openai.yaml exists, deletes it.
|
|
3216
|
+
* - If agents/ is now empty, removes the directory; if it still contains
|
|
3217
|
+
* other files (e.g. user-added content), leaves it in place.
|
|
3218
|
+
* - Non-gsd-* dirs and their agents/ content are never touched.
|
|
3219
|
+
* - Individual failures are caught and swallowed so a single bad dir cannot
|
|
3220
|
+
* block the install (fail-open, matching the original design).
|
|
3245
3221
|
*
|
|
3246
3222
|
* @param {string} skillsDir - Path to the skills/ directory (e.g. ~/.codex/skills)
|
|
3247
3223
|
*/
|
|
3248
|
-
function
|
|
3224
|
+
function cleanupCodexSkillMetadataSidecars(skillsDir) {
|
|
3249
3225
|
if (!fs.existsSync(skillsDir)) return;
|
|
3250
3226
|
// Mirror the user-owned list from installOpencodeFamilySkills (#2973).
|
|
3251
3227
|
// We MUST skip these dirs — their contents are user-generated and must
|
|
3252
|
-
// never be
|
|
3228
|
+
// never be modified by GSD's install path.
|
|
3253
3229
|
const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
|
|
3254
3230
|
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
3255
3231
|
if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue;
|
|
3256
3232
|
if (_userOwnedSkillDirs.has(entry.name)) continue; // preserve user content
|
|
3257
|
-
const
|
|
3258
|
-
const
|
|
3233
|
+
const agentsSubdir = path.join(skillsDir, entry.name, 'agents');
|
|
3234
|
+
const sidecarPath = path.join(agentsSubdir, 'openai.yaml');
|
|
3259
3235
|
try {
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
if (
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
const metaBlock = metaMatch[1];
|
|
3272
|
-
const sdMatch = metaBlock.match(/^[ \t]+short-description\s*:\s*(.+)$/m);
|
|
3273
|
-
if (sdMatch) {
|
|
3274
|
-
// Unescape YAML double-quoted scalar escapes before embedding.
|
|
3275
|
-
// convertClaudeCommandToCodexSkill always emits a double-quoted
|
|
3276
|
-
// value (via yamlQuote) so only double-quote unescaping is needed.
|
|
3277
|
-
let raw = sdMatch[1].trim();
|
|
3278
|
-
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
3279
|
-
// Strip outer double-quotes and decode \" → " and \\ → \
|
|
3280
|
-
raw = raw.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
3281
|
-
} else {
|
|
3282
|
-
// Single-quoted or unquoted: strip surrounding quotes/whitespace
|
|
3283
|
-
raw = raw.replace(/^["']|["']$/g, '');
|
|
3284
|
-
}
|
|
3285
|
-
shortDesc = raw;
|
|
3286
|
-
}
|
|
3287
|
-
}
|
|
3288
|
-
if (!shortDesc) {
|
|
3289
|
-
shortDesc = extractFrontmatterField(frontmatter, 'description') || '';
|
|
3290
|
-
}
|
|
3236
|
+
// Symlink guard: if agents/ is a symlink pointing outside the skills tree,
|
|
3237
|
+
// deleting through it could escape the tree. Skip this dir entirely.
|
|
3238
|
+
let agentsStat;
|
|
3239
|
+
try { agentsStat = fs.lstatSync(agentsSubdir); } catch (_e) { continue; }
|
|
3240
|
+
if (agentsStat.isSymbolicLink()) continue;
|
|
3241
|
+
if (fs.existsSync(sidecarPath)) {
|
|
3242
|
+
fs.rmSync(sidecarPath);
|
|
3243
|
+
}
|
|
3244
|
+
// Prune the agents/ dir only if it is now empty (leave it if other files remain).
|
|
3245
|
+
if (fs.existsSync(agentsSubdir) && fs.readdirSync(agentsSubdir).length === 0) {
|
|
3246
|
+
fs.rmdirSync(agentsSubdir);
|
|
3291
3247
|
}
|
|
3292
|
-
if (!shortDesc) {
|
|
3293
|
-
shortDesc = `Run GSD workflow ${entry.name}.`;
|
|
3294
|
-
}
|
|
3295
|
-
const yamlContent = generateCodexSkillMetadataYaml(entry.name, shortDesc);
|
|
3296
|
-
const agentsSubdir = path.join(skillDir, 'agents');
|
|
3297
|
-
fs.mkdirSync(agentsSubdir, { recursive: true });
|
|
3298
|
-
fs.writeFileSync(path.join(agentsSubdir, 'openai.yaml'), yamlContent);
|
|
3299
3248
|
} catch (_err) {
|
|
3300
|
-
// Fail open —
|
|
3249
|
+
// Fail open — a single bad dir must not block the install.
|
|
3301
3250
|
}
|
|
3302
3251
|
}
|
|
3303
3252
|
}
|
|
@@ -6843,6 +6792,12 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
|
|
|
6843
6792
|
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6844
6793
|
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6845
6794
|
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6795
|
+
// Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
|
|
6796
|
+
// .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
|
|
6797
|
+
// fires between 'e' and '-', which rewrites .claude-plugin → .cursor-plugin).
|
|
6798
|
+
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6799
|
+
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6800
|
+
content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
|
|
6846
6801
|
content = content.replace(/~\/\.cursor\//g, pathPrefix);
|
|
6847
6802
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6848
6803
|
break;
|
|
@@ -9837,14 +9792,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9837
9792
|
const scope = isGlobal ? 'global' : 'local';
|
|
9838
9793
|
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile);
|
|
9839
9794
|
|
|
9840
|
-
// #
|
|
9841
|
-
//
|
|
9842
|
-
//
|
|
9843
|
-
//
|
|
9844
|
-
//
|
|
9845
|
-
//
|
|
9795
|
+
// #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
|
|
9796
|
+
// gsd-* skill dirs. Prior installs wrote these files so Codex would show a
|
|
9797
|
+
// display name and description in the /skills TUI popup. Recent Codex builds
|
|
9798
|
+
// index BOTH SKILL.md and the sidecar, causing each GSD skill to appear twice
|
|
9799
|
+
// in autocomplete. Cleaning them up fixes the duplication; SKILL.md alone is
|
|
9800
|
+
// sufficient for Codex discovery. User-owned dirs are never touched.
|
|
9846
9801
|
if (isCodex) {
|
|
9847
|
-
|
|
9802
|
+
cleanupCodexSkillMetadataSidecars(path.join(targetDir, 'skills'));
|
|
9848
9803
|
}
|
|
9849
9804
|
|
|
9850
9805
|
// Hermes only: write DESCRIPTION.md for the gsd/ category after layout install
|
|
@@ -12126,8 +12081,7 @@ module.exports = {
|
|
|
12126
12081
|
convertClaudeToGeminiAgent,
|
|
12127
12082
|
convertClaudeAgentToCodexAgent,
|
|
12128
12083
|
generateCodexAgentToml,
|
|
12129
|
-
|
|
12130
|
-
writeCodexSkillMetadataFiles,
|
|
12084
|
+
cleanupCodexSkillMetadataSidecars,
|
|
12131
12085
|
generateCodexConfigBlock,
|
|
12132
12086
|
stripGsdFromCodexConfig,
|
|
12133
12087
|
migrateCodexHooksMapFormat,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: gsd:plan-phase
|
|
3
3
|
description: Create detailed phase plan (PLAN.md) with verification loop
|
|
4
4
|
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--tdd] [--mvp]"
|
|
5
|
-
effort:
|
|
5
|
+
effort: max
|
|
6
6
|
allowed-tools:
|
|
7
7
|
- Read
|
|
8
8
|
- Write
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-core",
|
|
3
|
-
"version": "1.5.0
|
|
3
|
+
"version": "1.5.0",
|
|
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
|
}
|
|
@@ -1519,6 +1519,17 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
1519
1519
|
break;
|
|
1520
1520
|
}
|
|
1521
1521
|
|
|
1522
|
+
// ─── teams-status ──────────────────────────────────────────────────────
|
|
1523
|
+
// Read-only detector for claude-code's experimental agent-teams feature.
|
|
1524
|
+
// issue #1355: stop gsd-core hanging silently under claude-code agent-teams.
|
|
1525
|
+
// No capability registration needed — this is a diagnostic query command,
|
|
1526
|
+
// not a feature capability.
|
|
1527
|
+
case 'teams-status': {
|
|
1528
|
+
const teamsStatus = require('./lib/teams-status.cjs');
|
|
1529
|
+
teamsStatus.cmdTeamsStatus(cwd, { active: args.includes('--active') });
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1522
1533
|
// ─── detect-custom-files ───────────────────────────────────────────────
|
|
1523
1534
|
// CJS-native: no SDK counterpart exists in the command registry.
|
|
1524
1535
|
// detect-custom-files reads a gsd-file-manifest.json against the
|
|
@@ -1566,18 +1577,30 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
1566
1577
|
|
|
1567
1578
|
const manifestKeys = new Set(Object.keys(manifest.files || {}));
|
|
1568
1579
|
|
|
1569
|
-
// GSD-managed directories to scan for user-added files.
|
|
1570
|
-
//
|
|
1571
|
-
|
|
1580
|
+
// GSD-managed directories to scan for user-added files. Whole-owned
|
|
1581
|
+
// roots are wiped recursively; shared runtime roots are pruned by the
|
|
1582
|
+
// same gsd-* top-level prefix used by install.js _removeGsdEntries.
|
|
1583
|
+
const GSD_WHOLE_MANAGED_DIRS = [
|
|
1572
1584
|
'gsd-core',
|
|
1573
|
-
'agents',
|
|
1574
1585
|
path.join('commands', 'gsd'),
|
|
1586
|
+
];
|
|
1587
|
+
const GSD_PREFIX_MANAGED_DIRS = [
|
|
1588
|
+
'agents',
|
|
1575
1589
|
'hooks',
|
|
1576
1590
|
'skills',
|
|
1577
1591
|
];
|
|
1578
1592
|
|
|
1579
1593
|
function collectCustomFiles(dir, baseDir, manifestKeys, out) {
|
|
1580
1594
|
if (!fs.existsSync(dir)) return;
|
|
1595
|
+
const stat = fs.statSync(dir);
|
|
1596
|
+
if (stat.isFile()) {
|
|
1597
|
+
const relPath = path.relative(baseDir, dir).replace(/\\/g, '/');
|
|
1598
|
+
if (!manifestKeys.has(relPath)) {
|
|
1599
|
+
out.push(relPath);
|
|
1600
|
+
}
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
if (!stat.isDirectory()) return;
|
|
1581
1604
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1582
1605
|
const fullPath = path.join(dir, entry.name);
|
|
1583
1606
|
if (entry.isDirectory()) {
|
|
@@ -1593,11 +1616,19 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
1593
1616
|
}
|
|
1594
1617
|
|
|
1595
1618
|
const customFiles = [];
|
|
1596
|
-
for (const managedDir of
|
|
1619
|
+
for (const managedDir of GSD_WHOLE_MANAGED_DIRS) {
|
|
1597
1620
|
const absDir = path.join(resolvedConfigDir, managedDir);
|
|
1598
1621
|
if (!fs.existsSync(absDir)) continue;
|
|
1599
1622
|
collectCustomFiles(absDir, resolvedConfigDir, manifestKeys, customFiles);
|
|
1600
1623
|
}
|
|
1624
|
+
for (const managedDir of GSD_PREFIX_MANAGED_DIRS) {
|
|
1625
|
+
const absDir = path.join(resolvedConfigDir, managedDir);
|
|
1626
|
+
if (!fs.existsSync(absDir)) continue;
|
|
1627
|
+
for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
|
|
1628
|
+
if (!entry.name.startsWith('gsd-')) continue;
|
|
1629
|
+
collectCustomFiles(path.join(absDir, entry.name), resolvedConfigDir, manifestKeys, customFiles);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1601
1632
|
|
|
1602
1633
|
const out = {
|
|
1603
1634
|
custom_files: customFiles,
|
|
@@ -61,7 +61,13 @@ function parseDecisions(content) {
|
|
|
61
61
|
// in addition to numeric-only IDs (D-42). The first character after `D-` must
|
|
62
62
|
// be alphanumeric, so malformed shapes like `D--foo` or `D-_bar` are rejected.
|
|
63
63
|
// CJS callers consume {id, text} and ignore the optional extras.
|
|
64
|
-
|
|
64
|
+
// #1343: `[^:*]*` replaces the old `\s*` before `:**` so that a freeform run
|
|
65
|
+
// such as `(parenthetical)`, an em-dash, or other prose between the optional
|
|
66
|
+
// bracket-tag group and the closing `:**` is tolerated rather than silently
|
|
67
|
+
// dropping the whole decision. `[^:*]*` subsumes plain whitespace and stops
|
|
68
|
+
// correctly at `:**`. Capture groups 1 (id), 2 (bracket tags), 3 (text) are
|
|
69
|
+
// unchanged.
|
|
70
|
+
const bulletRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^:*]*:\*\*\s*(.*)$/;
|
|
65
71
|
let current = null;
|
|
66
72
|
const flush = () => {
|
|
67
73
|
if (current) {
|
|
@@ -102,6 +108,18 @@ function parseDecisions(content) {
|
|
|
102
108
|
current = { id, text: bulletMatch[3], category, tags, trackable };
|
|
103
109
|
continue;
|
|
104
110
|
}
|
|
111
|
+
// Parse-miss guard (#1343): a line that looks like a `D-NN` decision bullet
|
|
112
|
+
// but failed `bulletRe` (e.g. a `:` or `*` inside the pre-colon run) must NOT
|
|
113
|
+
// be silently dropped — a narrowed trackable set lets a blocking coverage gate
|
|
114
|
+
// report a false pass. Surface it loudly instead.
|
|
115
|
+
if (/^\s*-\s+\*\*D-/.test(line)) {
|
|
116
|
+
// A malformed D-bullet still starts a (failed) new decision, so it ends the
|
|
117
|
+
// previous one — flush before warning so a following continuation line cannot
|
|
118
|
+
// be mis-appended to the prior valid decision.
|
|
119
|
+
flush();
|
|
120
|
+
console.warn(`parseDecisions: ignored unparseable decision bullet: ${trimmed}`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
105
123
|
// Continuation line for current decision (indented with space OR tab,
|
|
106
124
|
// non-bullet, non-empty) — tab indentation must work too (review F12).
|
|
107
125
|
if (current && trimmed !== '' && !trimmed.startsWith('-') && /^[ \t]/.test(line)) {
|
|
@@ -173,7 +173,7 @@ function extractPhaseToken(dirName) {
|
|
|
173
173
|
const tokenSegments = [];
|
|
174
174
|
for (let i = 0; i < segments.length; i++) {
|
|
175
175
|
const seg = segments[i];
|
|
176
|
-
if (/^\d/.test(seg)) {
|
|
176
|
+
if (/^\d/.test(seg) || (i === 0 && /^[A-Za-z]{1,3}\d/.test(seg))) {
|
|
177
177
|
tokenSegments.push(seg);
|
|
178
178
|
}
|
|
179
179
|
else {
|
|
@@ -1040,6 +1040,22 @@ function writePlanningFileSet(writes) {
|
|
|
1040
1040
|
throw err;
|
|
1041
1041
|
}
|
|
1042
1042
|
}
|
|
1043
|
+
function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
|
|
1044
|
+
if (!roadmapContent || !phaseNum)
|
|
1045
|
+
return null;
|
|
1046
|
+
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
|
|
1047
|
+
const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}\\s*:\\s*([^\\n]+)`, 'im'));
|
|
1048
|
+
if (!heading)
|
|
1049
|
+
return null;
|
|
1050
|
+
const name = heading[1].replace(/\(INSERTED\)/i, '').trim();
|
|
1051
|
+
return name || null;
|
|
1052
|
+
}
|
|
1053
|
+
function phaseDisplayNameFromSlug(slug) {
|
|
1054
|
+
if (!slug)
|
|
1055
|
+
return null;
|
|
1056
|
+
const name = slug.replace(/-/g, ' ').trim();
|
|
1057
|
+
return name || null;
|
|
1058
|
+
}
|
|
1043
1059
|
function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
1044
1060
|
if (!phaseNum) {
|
|
1045
1061
|
error('phase number required for phase complete');
|
|
@@ -1287,6 +1303,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1287
1303
|
const originalStateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
1288
1304
|
let stateContent = originalStateContent;
|
|
1289
1305
|
const phaseValue = nextPhaseNum || phaseNum;
|
|
1306
|
+
const nextPhaseDisplayName = phaseDisplayNameFromRoadmap(roadmapContent, nextPhaseNum) ??
|
|
1307
|
+
phaseDisplayNameFromSlug(nextPhaseName);
|
|
1290
1308
|
const existingPhaseField = stateExtractField(stateContent, 'Current Phase') ||
|
|
1291
1309
|
stateExtractField(stateContent, 'Phase');
|
|
1292
1310
|
let newPhaseValue = String(phaseValue);
|
|
@@ -1295,22 +1313,39 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1295
1313
|
const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
|
|
1296
1314
|
if (totalMatch) {
|
|
1297
1315
|
const total = totalMatch[1];
|
|
1298
|
-
const nameStr =
|
|
1299
|
-
? ` (${
|
|
1316
|
+
const nameStr = nextPhaseDisplayName
|
|
1317
|
+
? ` (${nextPhaseDisplayName})`
|
|
1300
1318
|
: nameMatch
|
|
1301
1319
|
? ` (${nameMatch[1]})`
|
|
1302
1320
|
: '';
|
|
1303
1321
|
newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
|
|
1304
1322
|
}
|
|
1323
|
+
else if (nextPhaseDisplayName) {
|
|
1324
|
+
newPhaseValue = `${phaseValue} — ${nextPhaseDisplayName}`;
|
|
1325
|
+
}
|
|
1305
1326
|
}
|
|
1306
1327
|
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
|
|
1307
|
-
if (
|
|
1308
|
-
stateContent =
|
|
1328
|
+
if (nextPhaseDisplayName) {
|
|
1329
|
+
stateContent =
|
|
1330
|
+
stateReplaceField(stateContent, 'Current Phase Name', nextPhaseDisplayName) ||
|
|
1331
|
+
stateContent;
|
|
1309
1332
|
}
|
|
1310
1333
|
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, isLastPhase ? 'Milestone complete' : 'Ready to plan');
|
|
1311
1334
|
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
|
|
1312
|
-
|
|
1313
|
-
|
|
1335
|
+
const lastActivityDescription = `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`;
|
|
1336
|
+
if (/^Last activity:/m.test(stateContent)) {
|
|
1337
|
+
stateContent =
|
|
1338
|
+
stateReplaceField(stateContent, 'Last activity', `${today} — ${lastActivityDescription}`) ||
|
|
1339
|
+
stateContent;
|
|
1340
|
+
}
|
|
1341
|
+
else {
|
|
1342
|
+
stateContent =
|
|
1343
|
+
stateReplaceField(stateContent, 'Last Activity', today) ||
|
|
1344
|
+
stateContent;
|
|
1345
|
+
}
|
|
1346
|
+
stateContent =
|
|
1347
|
+
stateReplaceField(stateContent, 'Last Activity Description', lastActivityDescription) ||
|
|
1348
|
+
stateContent;
|
|
1314
1349
|
const completedRaw = stateExtractField(stateContent, 'Completed Phases');
|
|
1315
1350
|
if (completedRaw !== null) {
|
|
1316
1351
|
let newCompleted = parseInt(completedRaw, 10);
|
|
@@ -281,6 +281,13 @@ function projectProhibitions(items) {
|
|
|
281
281
|
if (kind === 'lint-rule' && typeof p.check_rule === 'string' && p.check_rule.trim() !== '') {
|
|
282
282
|
entry.check_rule = String(p.check_rule);
|
|
283
283
|
}
|
|
284
|
+
// `check_violation_fixture` (#1346) rides BOTH kinds — it's what the #1279 prover machine-proves
|
|
285
|
+
// fail-first against. Emit ONLY a non-empty fixture (a blank one projects absent so green still
|
|
286
|
+
// hard-gates downstream — never a partial green); meaningless without the descriptor, so it lives
|
|
287
|
+
// inside this well-formed-descriptor branch.
|
|
288
|
+
if (typeof p.check_violation_fixture === 'string' && p.check_violation_fixture.trim() !== '') {
|
|
289
|
+
entry.check_violation_fixture = String(p.check_violation_fixture);
|
|
290
|
+
}
|
|
284
291
|
}
|
|
285
292
|
out.push(entry);
|
|
286
293
|
}
|