get-shit-done-cc 1.42.2 → 1.42.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/agents/gsd-executor.md +25 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-planner.md +2 -2
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.md +1 -1
- package/bin/install.js +101 -24
- package/get-shit-done/bin/lib/commands.cjs +1 -0
- package/get-shit-done/bin/lib/config.cjs +11 -1
- package/get-shit-done/bin/lib/core.cjs +33 -0
- package/get-shit-done/bin/lib/init.cjs +15 -0
- package/get-shit-done/bin/lib/installer-migration-report.cjs +234 -2
- package/get-shit-done/bin/lib/phase-command-router.cjs +1 -0
- package/get-shit-done/bin/lib/phase.cjs +89 -3
- package/get-shit-done/bin/lib/roadmap.cjs +25 -3
- package/get-shit-done/workflows/execute-phase.md +1 -1
- package/get-shit-done/workflows/plan-phase.md +45 -2
- package/get-shit-done/workflows/ultraplan-phase.md +10 -1
- package/get-shit-done/workflows/update.md +9 -3
- package/package.json +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +49 -3
- package/sdk/dist/query/config-mutation.d.ts.map +1 -1
- package/sdk/dist/query/config-mutation.js +7 -0
- package/sdk/dist/query/config-mutation.js.map +1 -1
- package/sdk/dist/query/init.d.ts.map +1 -1
- package/sdk/dist/query/init.js +12 -0
- package/sdk/dist/query/init.js.map +1 -1
- package/sdk/dist/query/state.d.ts.map +1 -1
- package/sdk/dist/query/state.js +13 -0
- package/sdk/dist/query/state.js.map +1 -1
- package/sdk/dist/query/validate.d.ts.map +1 -1
- package/sdk/dist/query/validate.js +37 -6
- package/sdk/dist/query/validate.js.map +1 -1
- package/sdk/dist/query-gsd-tools-runtime.d.ts.map +1 -1
- package/sdk/dist/query-gsd-tools-runtime.js +7 -1
- package/sdk/dist/query-gsd-tools-runtime.js.map +1 -1
- package/sdk/package-lock.json +2 -2
- package/sdk/package.json +1 -1
- package/sdk/src/bug-3591-gsdtools-runtime-workstream.test.ts +179 -0
- package/sdk/src/query/config-mutation.ts +7 -0
- package/sdk/src/query/init.test.ts +48 -0
- package/sdk/src/query/init.ts +13 -0
- package/sdk/src/query/state.ts +12 -0
- package/sdk/src/query/validate.test.ts +67 -0
- package/sdk/src/query/validate.ts +34 -6
- package/sdk/src/query-gsd-tools-runtime.ts +7 -1
- package/sdk-bundle/gsd-sdk.tgz +0 -0
package/agents/gsd-executor.md
CHANGED
|
@@ -192,7 +192,7 @@ This exclusion exists because a failed install may indicate a slopsquatted or ha
|
|
|
192
192
|
`[package-name]` could not be installed. Before proceeding:
|
|
193
193
|
1. Verify the package exists and is legitimate: https://npmjs.com/package/[package-name]
|
|
194
194
|
2. Confirm the package name is spelled correctly in PLAN.md
|
|
195
|
-
3. If the package does not exist,
|
|
195
|
+
3. If the package does not exist, re-run /gsd:plan-phase --research-phase <N> to find the correct package
|
|
196
196
|
</how-to-verify>
|
|
197
197
|
<resume-signal>Type "verified" with the correct package name, or "abort" to stop the phase</resume-signal>
|
|
198
198
|
</task>
|
|
@@ -551,6 +551,30 @@ back, those deletions appear on the main branch, destroying prior-wave work (#20
|
|
|
551
551
|
`<worktree_branch_check>` and per-commit `<pre_commit_head_assertion>` are the
|
|
552
552
|
correct prevention; if either fails, the workflow MUST stop, not self-heal.
|
|
553
553
|
- `git push --force` / `git push -f` to any branch you did not create.
|
|
554
|
+
- `git stash`, `git stash push`, `git stash pop`, `git stash apply`, `git stash drop`
|
|
555
|
+
(and any other `git stash` subcommand). **The stash list is shared across the
|
|
556
|
+
main checkout and every linked worktree** — git stores stashes at `refs/stash`
|
|
557
|
+
inside the parent `.git/` directory, not inside the per-worktree
|
|
558
|
+
`.git/worktrees/<name>/` subdirectory. From inside your worktree, `git stash list`
|
|
559
|
+
shows the global stack with no indication that entries originated elsewhere, and
|
|
560
|
+
`git stash pop` pops the top of that global stack regardless of which worktree
|
|
561
|
+
pushed it. Running `git stash pop` after a `git stash` that printed "No local
|
|
562
|
+
changes to save" will silently apply WIP from a sibling worktree's prior
|
|
563
|
+
session — typically producing UU/UD merge-conflict states, phantom untracked
|
|
564
|
+
files, and a contaminated working tree that violates the `isolation="worktree"`
|
|
565
|
+
invariant of your execution (#3542).
|
|
566
|
+
|
|
567
|
+
**Sanctioned alternatives** when you need to set aside or inspect work without
|
|
568
|
+
touching `refs/stash`:
|
|
569
|
+
|
|
570
|
+
- **Move WIP off the working tree:** commit it to a throwaway branch you own
|
|
571
|
+
(e.g. `git checkout -b scratch-/<task>-wip && git add -A && git commit -m "wip"`),
|
|
572
|
+
then `git checkout <your-worktree-branch>` to return to your task. The
|
|
573
|
+
throwaway branch lives in the per-worktree branch namespace and never
|
|
574
|
+
collides with sibling worktrees.
|
|
575
|
+
- **Read-only inspection of another ref:** use `git show <ref>:<path>` to
|
|
576
|
+
print a file at any ref, or `git diff <ref> -- <path>` to compare. Neither
|
|
577
|
+
mutates `refs/stash` nor leaks state across worktrees.
|
|
554
578
|
|
|
555
579
|
If you need to discard changes to a specific file you modified during this task, use:
|
|
556
580
|
```bash
|
|
@@ -14,7 +14,7 @@ color: cyan
|
|
|
14
14
|
<role>
|
|
15
15
|
You are a GSD phase researcher. You answer "What do I need to know to PLAN this phase well?" and produce a single RESEARCH.md that the planner consumes.
|
|
16
16
|
|
|
17
|
-
Spawned by `/gsd:plan-phase` (integrated) or `/gsd-research-phase
|
|
17
|
+
Spawned by `/gsd:plan-phase` (integrated) or `/gsd:plan-phase --research-phase <N>` (standalone).
|
|
18
18
|
|
|
19
19
|
@~/.claude/get-shit-done/references/mandatory-initial-read.md
|
|
20
20
|
|
package/agents/gsd-planner.md
CHANGED
|
@@ -183,7 +183,7 @@ Discovery is MANDATORY unless you can prove current context exists.
|
|
|
183
183
|
- Level 2+: New library not in package.json, external API, "choose/select/evaluate" in description
|
|
184
184
|
- Level 3: "architecture/design/system", multiple external services, data modeling, auth design
|
|
185
185
|
|
|
186
|
-
For niche domains (3D
|
|
186
|
+
For niche domains (3D/games/audio/shaders/ML), suggest `/gsd:plan-phase --research-phase <N>` first.
|
|
187
187
|
|
|
188
188
|
</discovery_levels>
|
|
189
189
|
|
|
@@ -988,7 +988,7 @@ Use `phase_dir` from init context (already loaded in load_project_state).
|
|
|
988
988
|
|
|
989
989
|
```bash
|
|
990
990
|
cat "$phase_dir"/*-CONTEXT.md 2>/dev/null # From /gsd:discuss-phase
|
|
991
|
-
cat "$phase_dir"/*-RESEARCH.md 2>/dev/null #
|
|
991
|
+
cat "$phase_dir"/*-RESEARCH.md 2>/dev/null # Research output
|
|
992
992
|
cat "$phase_dir"/*-DISCOVERY.md 2>/dev/null # From mandatory discovery
|
|
993
993
|
```
|
|
994
994
|
|
|
@@ -112,7 +112,7 @@ This is the most important section. Based on combined research:
|
|
|
112
112
|
- Which pitfalls it must avoid
|
|
113
113
|
|
|
114
114
|
**Add research flags:**
|
|
115
|
-
- Which phases likely need `/gsd-research-phase
|
|
115
|
+
- Which phases likely need `/gsd:plan-phase --research-phase <N>` during planning?
|
|
116
116
|
- Which phases have well-documented patterns (skip research)?
|
|
117
117
|
|
|
118
118
|
## Step 5: Assess Confidence
|
package/agents/gsd-roadmapper.md
CHANGED
|
@@ -202,7 +202,7 @@ Track coverage as you go.
|
|
|
202
202
|
**Integer phases (1, 2, 3):** Planned milestone work.
|
|
203
203
|
|
|
204
204
|
**Decimal phases (2.1, 2.2):** Urgent insertions after planning.
|
|
205
|
-
- Created via `/gsd
|
|
205
|
+
- Created via `/gsd:phase insert`
|
|
206
206
|
- Execute between integers: 1 → 1.1 → 1.2 → 2
|
|
207
207
|
|
|
208
208
|
**Starting number:**
|
package/bin/install.js
CHANGED
|
@@ -32,6 +32,19 @@ const reset = '\x1b[0m';
|
|
|
32
32
|
// Codex config.toml constants
|
|
33
33
|
const GSD_CODEX_MARKER = '# GSD Agent Configuration \u2014 managed by get-shit-done installer';
|
|
34
34
|
const GSD_CODEX_HOOKS_OWNERSHIP_PREFIX = '# GSD codex_hooks ownership: ';
|
|
35
|
+
// Codex's hook-enabling feature flag (issue #3566). Codex itself marks
|
|
36
|
+
// `codex_hooks` as a `legacy_key` in codex-rs/features/src/legacy.rs; the
|
|
37
|
+
// canonical current key under [features] is `hooks`. The installer always
|
|
38
|
+
// emits the canonical key going forward, recognizes legacy aliases as
|
|
39
|
+
// equivalent during reinstall, and migrates them forward on rewrite. The
|
|
40
|
+
// audit-marker string above is intentionally unchanged so existing
|
|
41
|
+
// installs' ownership lines continue to round-trip.
|
|
42
|
+
const CODEX_HOOKS_FEATURE_KEY = 'hooks';
|
|
43
|
+
const CODEX_HOOKS_FEATURE_LEGACY_KEYS = ['codex_hooks'];
|
|
44
|
+
const CODEX_HOOKS_FEATURE_ALL_KEYS = [CODEX_HOOKS_FEATURE_KEY, ...CODEX_HOOKS_FEATURE_LEGACY_KEYS];
|
|
45
|
+
function isCodexHooksFeatureKey(key) {
|
|
46
|
+
return CODEX_HOOKS_FEATURE_ALL_KEYS.includes(key);
|
|
47
|
+
}
|
|
35
48
|
|
|
36
49
|
// Copilot instructions marker constants
|
|
37
50
|
const GSD_COPILOT_INSTRUCTIONS_MARKER = '<!-- GSD Configuration \u2014 managed by get-shit-done installer -->';
|
|
@@ -99,11 +112,13 @@ const {
|
|
|
99
112
|
stageAgentsForProfile,
|
|
100
113
|
} = require(path.join(_gsdLibDir, 'install-profiles.cjs'));
|
|
101
114
|
const {
|
|
115
|
+
applyInstallerMigrationPlan,
|
|
102
116
|
discoverInstallerMigrations,
|
|
103
117
|
runInstallerMigrations,
|
|
104
118
|
} = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
|
|
105
119
|
const {
|
|
106
120
|
assertInstallerMigrationsUnblocked,
|
|
121
|
+
resolveInstallerMigrationPromptsForNonTty,
|
|
107
122
|
summarizeInstallerMigrationResult,
|
|
108
123
|
} = require(path.join(_gsdLibDir, 'installer-migration-report.cjs'));
|
|
109
124
|
|
|
@@ -3228,7 +3243,7 @@ function stripCodexHooksFeatureAssignments(content, ownership = null) {
|
|
|
3228
3243
|
!record.startsInMultilineString &&
|
|
3229
3244
|
record.keySegments &&
|
|
3230
3245
|
record.keySegments.length === 1 &&
|
|
3231
|
-
record.keySegments[0]
|
|
3246
|
+
isCodexHooksFeatureKey(record.keySegments[0])
|
|
3232
3247
|
);
|
|
3233
3248
|
|
|
3234
3249
|
for (const record of codexHookRecords) {
|
|
@@ -3273,7 +3288,7 @@ function stripCodexHooksFeatureAssignments(content, ownership = null) {
|
|
|
3273
3288
|
record.keySegments &&
|
|
3274
3289
|
record.keySegments.length === 2 &&
|
|
3275
3290
|
record.keySegments[0] === 'features' &&
|
|
3276
|
-
record.keySegments[1]
|
|
3291
|
+
isCodexHooksFeatureKey(record.keySegments[1])
|
|
3277
3292
|
);
|
|
3278
3293
|
|
|
3279
3294
|
for (const record of rootCodexHookRecords) {
|
|
@@ -4429,6 +4444,13 @@ function rewriteTomlKeyLines(content, matches, key) {
|
|
|
4429
4444
|
const blockEol = blockEnd > 0 && content[blockEnd - 1] === '\n'
|
|
4430
4445
|
? (blockEnd > 1 && content[blockEnd - 2] === '\r' ? '\r\n' : '\n')
|
|
4431
4446
|
: '';
|
|
4447
|
+
// Preserve the existing key when one is present on the line
|
|
4448
|
+
// (`match.keyRaw`). This respects user ownership: a user-authored
|
|
4449
|
+
// `codex_hooks = true` line stays as `codex_hooks = true` even
|
|
4450
|
+
// though `hooks` is the canonical key in current Codex (#3566).
|
|
4451
|
+
// Codex's own `legacy_key` alias mechanism in codex-rs handles the
|
|
4452
|
+
// backward compat at the runtime layer. Migration to canonical is
|
|
4453
|
+
// a fresh-insert-only operation in ensureCodexHooksFeature.
|
|
4432
4454
|
rewritten += normalizeCodexHooksLine(match.text, match.keyRaw || key) + blockEol;
|
|
4433
4455
|
cursor = blockEnd;
|
|
4434
4456
|
return;
|
|
@@ -4610,11 +4632,17 @@ function ensureCodexHooksFeature(configContent) {
|
|
|
4610
4632
|
record.end + record.eol.length <= featuresSection.end &&
|
|
4611
4633
|
record.keySegments &&
|
|
4612
4634
|
record.keySegments.length === 1 &&
|
|
4613
|
-
record.keySegments[0]
|
|
4635
|
+
isCodexHooksFeatureKey(record.keySegments[0])
|
|
4614
4636
|
);
|
|
4615
4637
|
|
|
4616
4638
|
if (sectionLines.length > 0) {
|
|
4617
|
-
|
|
4639
|
+
// Rewrite to canonical key — this migrates legacy `codex_hooks` to
|
|
4640
|
+
// `hooks` in-place on every reinstall. If the file already has the
|
|
4641
|
+
// canonical key the rewrite is a no-op shape-wise (same key, same
|
|
4642
|
+
// value). The rewriteTomlKeyLines helper preserves indentation,
|
|
4643
|
+
// trailing comments, and ownership-marker positioning, and always
|
|
4644
|
+
// emits the caller-supplied canonical key (#3566).
|
|
4645
|
+
const rewritten = rewriteTomlKeyLines(configContent, sectionLines, CODEX_HOOKS_FEATURE_KEY);
|
|
4618
4646
|
return {
|
|
4619
4647
|
content: repairTrappedFeaturesKeys(rewritten),
|
|
4620
4648
|
ownership: null,
|
|
@@ -4624,7 +4652,7 @@ function ensureCodexHooksFeature(configContent) {
|
|
|
4624
4652
|
const sectionBody = configContent.slice(featuresSection.headerEnd, featuresSection.end);
|
|
4625
4653
|
const needsSeparator = sectionBody.length > 0 && !sectionBody.endsWith('\n') && !sectionBody.endsWith('\r\n');
|
|
4626
4654
|
const insertPrefix = sectionBody.length === 0 && featuresSection.headerEnd === configContent.length ? eol : '';
|
|
4627
|
-
const insertText = `${insertPrefix}${needsSeparator ? eol : ''}
|
|
4655
|
+
const insertText = `${insertPrefix}${needsSeparator ? eol : ''}${CODEX_HOOKS_FEATURE_KEY} = true${eol}`;
|
|
4628
4656
|
const merged = configContent.slice(0, featuresSection.end) + insertText + configContent.slice(featuresSection.end);
|
|
4629
4657
|
return {
|
|
4630
4658
|
content: repairTrappedFeaturesKeys(merged),
|
|
@@ -4642,11 +4670,11 @@ function ensureCodexHooksFeature(configContent) {
|
|
|
4642
4670
|
);
|
|
4643
4671
|
|
|
4644
4672
|
const rootCodexHooksLines = rootFeatureLines
|
|
4645
|
-
.filter((record) => record.keySegments.length === 2 && record.keySegments[1]
|
|
4673
|
+
.filter((record) => record.keySegments.length === 2 && isCodexHooksFeatureKey(record.keySegments[1]));
|
|
4646
4674
|
|
|
4647
4675
|
if (rootCodexHooksLines.length > 0) {
|
|
4648
4676
|
return {
|
|
4649
|
-
content: rewriteTomlKeyLines(configContent, rootCodexHooksLines,
|
|
4677
|
+
content: rewriteTomlKeyLines(configContent, rootCodexHooksLines, `features.${CODEX_HOOKS_FEATURE_KEY}`),
|
|
4650
4678
|
ownership: null,
|
|
4651
4679
|
};
|
|
4652
4680
|
}
|
|
@@ -4664,13 +4692,13 @@ function ensureCodexHooksFeature(configContent) {
|
|
|
4664
4692
|
const prefix = insertAt > 0 && configContent[insertAt - 1] === '\n' ? '' : eol;
|
|
4665
4693
|
return {
|
|
4666
4694
|
content: configContent.slice(0, insertAt) +
|
|
4667
|
-
`${prefix}features
|
|
4695
|
+
`${prefix}features.${CODEX_HOOKS_FEATURE_KEY} = true${eol}` +
|
|
4668
4696
|
configContent.slice(insertAt),
|
|
4669
4697
|
ownership: 'root_dotted',
|
|
4670
4698
|
};
|
|
4671
4699
|
}
|
|
4672
4700
|
|
|
4673
|
-
const featuresBlock = `[features]${eol}
|
|
4701
|
+
const featuresBlock = `[features]${eol}${CODEX_HOOKS_FEATURE_KEY} = true${eol}`;
|
|
4674
4702
|
if (!configContent) {
|
|
4675
4703
|
return { content: featuresBlock, ownership: 'section' };
|
|
4676
4704
|
}
|
|
@@ -4701,11 +4729,11 @@ function hasEnabledCodexHooksFeature(configContent) {
|
|
|
4701
4729
|
|
|
4702
4730
|
const isSectionKey = record.tablePath === 'features' &&
|
|
4703
4731
|
record.keySegments.length === 1 &&
|
|
4704
|
-
record.keySegments[0]
|
|
4732
|
+
isCodexHooksFeatureKey(record.keySegments[0]);
|
|
4705
4733
|
const isRootDottedKey = record.tablePath === null &&
|
|
4706
4734
|
record.keySegments.length === 2 &&
|
|
4707
4735
|
record.keySegments[0] === 'features' &&
|
|
4708
|
-
record.keySegments[1]
|
|
4736
|
+
isCodexHooksFeatureKey(record.keySegments[1]);
|
|
4709
4737
|
|
|
4710
4738
|
if (!isSectionKey && !isRootDottedKey) {
|
|
4711
4739
|
return false;
|
|
@@ -8030,6 +8058,51 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8030
8058
|
migrations: options.installerMigrations,
|
|
8031
8059
|
baselineScan: true,
|
|
8032
8060
|
});
|
|
8061
|
+
// #3541: non-interactive runs (typical /gsd-update via Claude Code) have
|
|
8062
|
+
// no stdin TTY and therefore no way to answer prompt-user migration
|
|
8063
|
+
// actions. Resolve safe categories by classification (stale SDK build
|
|
8064
|
+
// artifacts → remove; user-facing skills → keep; bundled GSD hooks →
|
|
8065
|
+
// remove [#3610]) and log every resolution; anything that cannot be
|
|
8066
|
+
// safely defaulted falls through to assertInstallerMigrationsUnblocked,
|
|
8067
|
+
// which now emits a grouped error with the documented resolution path.
|
|
8068
|
+
//
|
|
8069
|
+
// #3610: the classifier-based resolution must run regardless of TTY.
|
|
8070
|
+
// For unambiguous categories (e.g. `hooks/gsd-*` bundled hooks left
|
|
8071
|
+
// behind by a previous version), there is no actual "user choice" to
|
|
8072
|
+
// make — the file is a known GSD-managed artifact and the installer is
|
|
8073
|
+
// about to write the fresh bundled version. Gating the resolver on
|
|
8074
|
+
// `!isTTY` made `npx get-shit-done-cc@latest --codex` hard-abort with
|
|
8075
|
+
// 12 blocked bundled hooks. The env-override branch (operator-supplied
|
|
8076
|
+
// GSD_INSTALLER_MIGRATION_RESOLVE) still applies only in non-TTY mode.
|
|
8077
|
+
const _migrationIsTty = process.stdin && process.stdin.isTTY === true;
|
|
8078
|
+
if (Array.isArray(installerMigrationResult.blocked) &&
|
|
8079
|
+
installerMigrationResult.blocked.length > 0 &&
|
|
8080
|
+
installerMigrationResult.plan &&
|
|
8081
|
+
Array.isArray(installerMigrationResult.plan.actions)) {
|
|
8082
|
+
const { resolutions } = resolveInstallerMigrationPromptsForNonTty(
|
|
8083
|
+
installerMigrationResult,
|
|
8084
|
+
{ isTty: false }
|
|
8085
|
+
);
|
|
8086
|
+
for (const entry of resolutions) {
|
|
8087
|
+
console.log(
|
|
8088
|
+
` ↪ installer-migration auto-resolved: ${entry.relPath} → ${entry.choice} ` +
|
|
8089
|
+
`(category=${entry.category}, source=${entry.source})`
|
|
8090
|
+
);
|
|
8091
|
+
}
|
|
8092
|
+
// If we resolved anything, the original run returned early without
|
|
8093
|
+
// applying the (now-unblocked) plan — apply it here.
|
|
8094
|
+
if (resolutions.length > 0 && installerMigrationResult.plan.blocked.length === 0) {
|
|
8095
|
+
const applyResult = applyInstallerMigrationPlan({
|
|
8096
|
+
configDir: targetDir,
|
|
8097
|
+
plan: installerMigrationResult.plan,
|
|
8098
|
+
});
|
|
8099
|
+
installerMigrationResult = {
|
|
8100
|
+
...installerMigrationResult,
|
|
8101
|
+
...applyResult,
|
|
8102
|
+
blocked: [],
|
|
8103
|
+
};
|
|
8104
|
+
}
|
|
8105
|
+
}
|
|
8033
8106
|
reportInstallerMigrationResult(installerMigrationResult);
|
|
8034
8107
|
assertInstallerMigrationsUnblocked(installerMigrationResult);
|
|
8035
8108
|
|
|
@@ -8049,23 +8122,27 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8049
8122
|
failures.push('command/gsd-*');
|
|
8050
8123
|
}
|
|
8051
8124
|
} else if (isCodex) {
|
|
8125
|
+
// Codex CLI (0.130.0 at time of #3562) does NOT auto-discover commands
|
|
8126
|
+
// from get-shit-done/workflows/*.md or agents/*.md. It only registers
|
|
8127
|
+
// commands from skills/<name>/SKILL.md. The earlier "Codex discovers
|
|
8128
|
+
// official skills directly" branch left users with workflows on disk and
|
|
8129
|
+
// no $gsd-* entrypoints. Regenerate the skill surface the same way the
|
|
8130
|
+
// other runtimes do — copyCommandsAsCodexSkills() rewrites each
|
|
8131
|
+
// commands/gsd/*.md as ~/.codex/skills/gsd-<name>/SKILL.md and converts
|
|
8132
|
+
// Claude-flavored command frontmatter into Codex skill frontmatter.
|
|
8052
8133
|
const skillsDir = path.join(targetDir, 'skills');
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
// gsd-* copies under ~/.codex/skills are therefore removed and no longer
|
|
8056
|
-
// regenerated.
|
|
8057
|
-
let removedLegacyCodexSkills = 0;
|
|
8134
|
+
const gsdSrc = _stageSkills(_commandsDir);
|
|
8135
|
+
copyCommandsAsCodexSkills(gsdSrc, skillsDir, 'gsd', pathPrefix, runtime);
|
|
8058
8136
|
if (fs.existsSync(skillsDir)) {
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8137
|
+
const count = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
8138
|
+
.filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
|
|
8139
|
+
if (count > 0) {
|
|
8140
|
+
console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
|
|
8141
|
+
} else {
|
|
8142
|
+
failures.push('skills/gsd-*');
|
|
8063
8143
|
}
|
|
8064
|
-
}
|
|
8065
|
-
if (removedLegacyCodexSkills > 0) {
|
|
8066
|
-
console.log(` ${green}✓${reset} Removed ${removedLegacyCodexSkills} legacy Codex gsd-* skill copies from skills/`);
|
|
8067
8144
|
} else {
|
|
8068
|
-
|
|
8145
|
+
failures.push('skills/gsd-*');
|
|
8069
8146
|
}
|
|
8070
8147
|
} else if (isCopilot) {
|
|
8071
8148
|
const skillsDir = path.join(targetDir, 'skills');
|
|
@@ -392,7 +392,17 @@ function setConfigValue(cwd, keyPath, parsedValue) {
|
|
|
392
392
|
*/
|
|
393
393
|
function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
394
394
|
if (!keyPath) {
|
|
395
|
-
error('Usage: config-set <key.path> <value>');
|
|
395
|
+
error('Usage: config-set <key.path> <value>', ERROR_REASON.USAGE);
|
|
396
|
+
}
|
|
397
|
+
// #3593: reject the "key without value" form (e.g. `config-set
|
|
398
|
+
// model_profile` with args[2] === undefined). Without this guard the
|
|
399
|
+
// value passes through as undefined, the number/boolean/json branches
|
|
400
|
+
// all fall through, and the write either silently strips the key
|
|
401
|
+
// (JSON.stringify drops undefined values) or writes a corrupt entry.
|
|
402
|
+
// Typed reason so the negative-matrix test can assert on it instead
|
|
403
|
+
// of greppinng prose.
|
|
404
|
+
if (value === undefined) {
|
|
405
|
+
error('Usage: config-set <key.path> <value>', ERROR_REASON.USAGE);
|
|
396
406
|
}
|
|
397
407
|
|
|
398
408
|
validateKnownConfigKeyPath(keyPath);
|
|
@@ -529,6 +529,7 @@ function loadConfig(cwd, options = {}) {
|
|
|
529
529
|
firecrawl: get('firecrawl') ?? defaults.firecrawl,
|
|
530
530
|
exa_search: get('exa_search') ?? defaults.exa_search,
|
|
531
531
|
tdd_mode: get('tdd_mode', { section: 'workflow', field: 'tdd_mode' }) ?? false,
|
|
532
|
+
mvp_mode: get('mvp_mode', { section: 'workflow', field: 'mvp_mode' }) ?? false,
|
|
532
533
|
text_mode: get('text_mode', { section: 'workflow', field: 'text_mode' }) ?? defaults.text_mode,
|
|
533
534
|
auto_advance: get('auto_advance', { section: 'workflow', field: 'auto_advance' }) ?? false,
|
|
534
535
|
_auto_chain_active: get('_auto_chain_active', { section: 'workflow', field: '_auto_chain_active' }) ?? false,
|
|
@@ -751,6 +752,25 @@ function phaseMarkdownRegexSource(phaseNum) {
|
|
|
751
752
|
return `0*${escapeRegex(integer)}${letter}${decimal}`;
|
|
752
753
|
}
|
|
753
754
|
|
|
755
|
+
/**
|
|
756
|
+
* #3599: when the caller passed a project-code-prefixed ID like `PROJ-42`,
|
|
757
|
+
* return the exact-escaped form so the caller can search the ROADMAP for
|
|
758
|
+
* `### Phase PROJ-42:` BEFORE falling back to the padding-tolerant numeric
|
|
759
|
+
* form. Returns null when the input has no project-code prefix — in that
|
|
760
|
+
* case the numeric form (`phaseMarkdownRegexSource`) is the only thing the
|
|
761
|
+
* caller needs.
|
|
762
|
+
*
|
|
763
|
+
* Two-pass at the call site preserves the #3537 contract (`CK-01` directory
|
|
764
|
+
* names mapping to `Phase 1:` prose) while letting `PROJ-42` resolve to its
|
|
765
|
+
* own prefixed heading without cross-matching a bare `### Phase 42:` that
|
|
766
|
+
* happens to share the trailing integer.
|
|
767
|
+
*/
|
|
768
|
+
function phaseMarkdownRegexSourceExact(phaseNum) {
|
|
769
|
+
const raw = String(phaseNum);
|
|
770
|
+
if (!/^[A-Z]{1,6}-(?=\d)/i.test(raw)) return null;
|
|
771
|
+
return escapeRegex(raw);
|
|
772
|
+
}
|
|
773
|
+
|
|
754
774
|
function comparePhaseNum(a, b) {
|
|
755
775
|
// Strip optional project_code prefix before comparing (e.g., 'CK-01-name' → '01-name')
|
|
756
776
|
const sa = String(a).replace(/^[A-Z]{1,6}-/, '');
|
|
@@ -1812,6 +1832,18 @@ function getMilestonePhaseFilter(cwd, versionOverride) {
|
|
|
1812
1832
|
// Try custom ID match (e.g. PROJ-42-description → PROJ-42)
|
|
1813
1833
|
const customMatch = dirName.match(/^([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)/);
|
|
1814
1834
|
if (customMatch && normalized.has(customMatch[1].toLowerCase())) return true;
|
|
1835
|
+
// #3600: project-code-prefixed directory (`CK-01-name`) against a
|
|
1836
|
+
// numeric ROADMAP heading (`### Phase 1:`). Strip the same prefix
|
|
1837
|
+
// shape `normalizePhaseName` recognises (`^[A-Z]{1,6}-(?=\d)`) and
|
|
1838
|
+
// retry the numeric match. This runs AFTER the custom-ID match so
|
|
1839
|
+
// a roadmap that uses `Phase PROJ-42:` continues to win via the
|
|
1840
|
+
// existing custom-ID path; the strip-and-retry only fires when the
|
|
1841
|
+
// milestone is keyed on the bare numeric form.
|
|
1842
|
+
const stripped = dirName.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
1843
|
+
if (stripped !== dirName) {
|
|
1844
|
+
const sm = stripped.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
|
|
1845
|
+
if (sm && normalized.has(sm[1].toLowerCase())) return true;
|
|
1846
|
+
}
|
|
1815
1847
|
return false;
|
|
1816
1848
|
}
|
|
1817
1849
|
isDirInMilestone.phaseCount = milestonePhaseNums.size;
|
|
@@ -1900,6 +1932,7 @@ module.exports = {
|
|
|
1900
1932
|
escapeRegex,
|
|
1901
1933
|
normalizePhaseName,
|
|
1902
1934
|
phaseMarkdownRegexSource,
|
|
1935
|
+
phaseMarkdownRegexSourceExact,
|
|
1903
1936
|
comparePhaseNum,
|
|
1904
1937
|
searchPhaseInDir,
|
|
1905
1938
|
extractPhaseToken,
|
|
@@ -10,6 +10,7 @@ const { planningPaths, planningDir, planningRoot } = require('./planning-workspa
|
|
|
10
10
|
const { maskIfSecret } = require('./secrets.cjs');
|
|
11
11
|
const scanPhasePlans = require('./plan-scan.cjs');
|
|
12
12
|
const { stateExtractField } = require('./state-document.cjs');
|
|
13
|
+
const { determinePhaseStatus } = require('./commands.cjs');
|
|
13
14
|
|
|
14
15
|
// Accept all bold/colon variants of the Requirements header (#2769):
|
|
15
16
|
// **Requirements:** / **Requirements**: / **Requirements** : render the
|
|
@@ -295,6 +296,20 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
295
296
|
padded_phase: phaseNumberPlan ? normalizePhaseName(phaseNumberPlan) : null,
|
|
296
297
|
phase_req_ids,
|
|
297
298
|
|
|
299
|
+
// #3569: surface phase lifecycle status so /gsd:plan-phase can short-circuit
|
|
300
|
+
// on closed (Complete) phases instead of silently replanning over shipped
|
|
301
|
+
// code. Reuses determinePhaseStatus — the project-wide vocabulary
|
|
302
|
+
// (Pending | Planned | In Progress | Executed | Complete | Needs Review).
|
|
303
|
+
// No directory yet → Pending (phase has not been started).
|
|
304
|
+
phase_status: phaseDirPlan
|
|
305
|
+
? determinePhaseStatus(
|
|
306
|
+
phaseInfo?.plans?.length || 0,
|
|
307
|
+
phaseInfo?.summaries?.length || 0,
|
|
308
|
+
path.join(cwd, phaseDirPlan),
|
|
309
|
+
'Pending',
|
|
310
|
+
)
|
|
311
|
+
: 'Pending',
|
|
312
|
+
|
|
298
313
|
// Existing artifacts
|
|
299
314
|
has_research: phaseInfo?.has_research || false,
|
|
300
315
|
has_context: phaseInfo?.has_context || false,
|