gsdd-cli 0.18.0 → 0.18.2
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/README.md +7 -6
- package/bin/gsdd.mjs +17 -14
- package/bin/lib/health-truth.mjs +8 -8
- package/bin/lib/health.mjs +29 -26
- package/bin/lib/init-flow.mjs +93 -34
- package/bin/lib/init-runtime.mjs +5 -4
- package/bin/lib/lifecycle-state.mjs +211 -28
- package/bin/lib/manifest.mjs +24 -20
- package/bin/lib/provenance.mjs +281 -59
- package/bin/lib/rendering.mjs +110 -23
- package/bin/lib/runtime-freshness.mjs +44 -21
- package/distilled/DESIGN.md +314 -6
- package/distilled/EVIDENCE-INDEX.md +70 -10
- package/distilled/README.md +2 -2
- package/distilled/workflows/audit-milestone.md +1 -1
- package/distilled/workflows/complete-milestone.md +1 -1
- package/distilled/workflows/execute.md +5 -5
- package/distilled/workflows/map-codebase.md +11 -11
- package/distilled/workflows/new-milestone.md +40 -24
- package/distilled/workflows/new-project.md +43 -26
- package/distilled/workflows/pause.md +1 -1
- package/distilled/workflows/plan.md +184 -73
- package/distilled/workflows/progress.md +103 -54
- package/distilled/workflows/quick.md +11 -10
- package/distilled/workflows/resume.md +140 -66
- package/distilled/workflows/verify.md +5 -5
- package/docs/RUNTIME-SUPPORT.md +2 -2
- package/docs/USER-GUIDE.md +11 -5
- package/package.json +19 -20
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npx gsdd-cli init
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
**Directly validated today:** Claude Code, Codex CLI, and OpenCode.
|
|
15
|
-
**Qualified support:** Cursor, Copilot, and Gemini CLI support the same core workflow through the shared `.agents/skills/` surface;
|
|
15
|
+
**Qualified support:** Cursor, Copilot, and Gemini CLI support the same core workflow through the shared `.agents/skills/` surface; they do not carry the same runtime proof or ergonomics.
|
|
16
16
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
@@ -63,7 +63,7 @@ Verification is a separate workflow with a separate context window, not a checkb
|
|
|
63
63
|
<details>
|
|
64
64
|
<summary>How it works</summary>
|
|
65
65
|
|
|
66
|
-
Invariant suites (I-series), guard suites (G-series), and scenario suites (S-series) run on every change. Each assertion includes a `FIX:` instruction so failures are actionable.
|
|
66
|
+
Invariant suites (I-series), guard suites (G-series), and scenario suites (S-series) run on every change. Each assertion includes a `FIX:` instruction so failures are actionable. 58 documented design decisions in `distilled/DESIGN.md` record the rationale with evidence trails.
|
|
67
67
|
|
|
68
68
|
</details>
|
|
69
69
|
|
|
@@ -102,13 +102,14 @@ In a terminal, `gsdd init` now opens a guided install wizard:
|
|
|
102
102
|
- Step 2: decide separately whether repo-wide `AGENTS.md` governance is worth installing
|
|
103
103
|
- Step 3: configure planning defaults in the same guided flow
|
|
104
104
|
|
|
105
|
-
Portable `.agents/skills/gsdd-*` skills are always generated. The wizard controls extra native adapters and optional governance, not the portable baseline.
|
|
106
|
-
When those generated surfaces exist locally, `gsdd health` checks them against current render output instead of asking you to trust manual review.
|
|
105
|
+
Portable `.agents/skills/gsdd-*` skills are always generated. The wizard controls extra native adapters and optional governance, not the portable baseline.
|
|
106
|
+
When those generated surfaces exist locally, `gsdd health` checks them against current render output instead of asking you to trust manual review.
|
|
107
|
+
`init` generates a local `.planning/bin/gsdd*` helper surface. Workflow-embedded helper commands still use `node .planning/bin/gsdd.mjs ...` as the portable contract, so execution-time lifecycle helpers do not depend on a global `gsdd` binary after init.
|
|
107
108
|
|
|
108
109
|
### Launch Proof Status
|
|
109
110
|
|
|
110
111
|
- **Directly validated:** Claude Code, Codex CLI, and OpenCode have recorded `plan -> execute -> verify` evidence for the core lifecycle.
|
|
111
|
-
- **Qualified support:** Cursor, Copilot, and Gemini CLI support the same core workflow through the shared `.agents/skills/` surface;
|
|
112
|
+
- **Qualified support:** Cursor, Copilot, and Gemini CLI support the same core workflow through the shared `.agents/skills/` surface; they do not carry the same runtime proof or ergonomics.
|
|
112
113
|
- **Runtime-surface freshness:** Installed generated skills and native adapters are renderer-checked locally; repair stays deterministic through `npx gsdd-cli update`.
|
|
113
114
|
|
|
114
115
|
Start with the public proof pack:
|
|
@@ -504,7 +505,7 @@ For detailed troubleshooting and recovery procedures, see the [User Guide](docs/
|
|
|
504
505
|
|
|
505
506
|
## Design Decisions
|
|
506
507
|
|
|
507
|
-
This repo records
|
|
508
|
+
This repo records 58 documented design decisions relative to GSD, each with evidence from source files and external research. See [`distilled/DESIGN.md`](distilled/DESIGN.md) for the full rationale.
|
|
508
509
|
|
|
509
510
|
Key choices:
|
|
510
511
|
- **4-file codebase standard** — drop state that rots (STRUCTURE, INTEGRATIONS, TESTING), keep rules that don't
|
package/bin/gsdd.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// gsdd - Workspine CLI
|
|
4
4
|
|
|
5
|
-
import { realpathSync } from 'fs';
|
|
5
|
+
import { realpathSync, readFileSync } from 'fs';
|
|
6
6
|
import { join, dirname } from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { createAdapterRegistry } from './adapters/index.mjs';
|
|
@@ -21,11 +21,12 @@ import { cmdFileOp } from './lib/file-ops.mjs';
|
|
|
21
21
|
import { createCmdHealth } from './lib/health.mjs';
|
|
22
22
|
import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
|
|
23
23
|
|
|
24
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
-
const __dirname = dirname(__filename);
|
|
26
|
-
const DISTILLED_DIR = join(__dirname, '..', 'distilled');
|
|
27
|
-
const AGENTS_DIR = join(__dirname, '..', 'agents');
|
|
28
|
-
const
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
const __dirname = dirname(__filename);
|
|
26
|
+
const DISTILLED_DIR = join(__dirname, '..', 'distilled');
|
|
27
|
+
const AGENTS_DIR = join(__dirname, '..', 'agents');
|
|
28
|
+
const PACKAGE_JSON = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
29
|
+
const CWD = process.cwd();
|
|
29
30
|
const IS_MAIN = process.argv[1]
|
|
30
31
|
? realpathSync(process.argv[1]) === realpathSync(__filename)
|
|
31
32
|
: false;
|
|
@@ -60,14 +61,16 @@ const WORKFLOWS = [
|
|
|
60
61
|
|
|
61
62
|
const FRAMEWORK_VERSION = 'v1.4';
|
|
62
63
|
|
|
63
|
-
function createCliContext(cwd = process.cwd()) {
|
|
64
|
-
return {
|
|
65
|
-
cwd,
|
|
66
|
-
planningDir: join(cwd, '.planning'),
|
|
67
|
-
distilledDir: DISTILLED_DIR,
|
|
68
|
-
agentsDir: AGENTS_DIR,
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
function createCliContext(cwd = process.cwd()) {
|
|
65
|
+
return {
|
|
66
|
+
cwd,
|
|
67
|
+
planningDir: join(cwd, '.planning'),
|
|
68
|
+
distilledDir: DISTILLED_DIR,
|
|
69
|
+
agentsDir: AGENTS_DIR,
|
|
70
|
+
packageName: PACKAGE_JSON.name,
|
|
71
|
+
packageVersion: PACKAGE_JSON.version,
|
|
72
|
+
workflows: WORKFLOWS,
|
|
73
|
+
frameworkVersion: FRAMEWORK_VERSION,
|
|
71
74
|
adapters: createAdapterRegistry({
|
|
72
75
|
cwd,
|
|
73
76
|
workflows: WORKFLOWS,
|
package/bin/lib/health-truth.mjs
CHANGED
|
@@ -87,14 +87,14 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, option
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
if (options.runtimeFreshnessReport?.issueCount > 0) {
|
|
91
|
-
warnings.push({
|
|
92
|
-
id: 'W11',
|
|
93
|
-
severity: 'WARN',
|
|
94
|
-
message: `Installed generated runtime surfaces drift from current render output (${summarizeRuntimeFreshnessIssues(options.runtimeFreshnessReport)})`,
|
|
95
|
-
fix: getRuntimeFreshnessRepairGuidance(options.runtimeFreshnessReport),
|
|
96
|
-
});
|
|
97
|
-
}
|
|
90
|
+
if (options.runtimeFreshnessReport?.issueCount > 0) {
|
|
91
|
+
warnings.push({
|
|
92
|
+
id: 'W11',
|
|
93
|
+
severity: 'WARN',
|
|
94
|
+
message: `Installed generated runtime and workflow-helper surfaces drift from current render output (${summarizeRuntimeFreshnessIssues(options.runtimeFreshnessReport)})`,
|
|
95
|
+
fix: getRuntimeFreshnessRepairGuidance(options.runtimeFreshnessReport),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
98
|
|
|
99
99
|
const drift = checkDrift(planningDir);
|
|
100
100
|
if (drift.drifted) {
|
package/bin/lib/health.mjs
CHANGED
|
@@ -21,7 +21,7 @@ export function createCmdHealth(ctx) {
|
|
|
21
21
|
const cwd = process.cwd();
|
|
22
22
|
const planningDir = join(cwd, '.planning');
|
|
23
23
|
const frameworkSourceMode = isFrameworkSourceRepo(cwd);
|
|
24
|
-
const healthCheckIds = ['E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'E7', 'E8', 'W1', 'W2', 'W3', 'W4', 'W5', 'W6', ...TRUTH_CHECK_IDS, 'I1', 'I2', 'I3'];
|
|
24
|
+
const healthCheckIds = ['E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'E7', 'E8', 'W1', 'W2', 'W3', 'W4', 'W5', 'W6', ...TRUTH_CHECK_IDS, 'I1', 'I2', 'I3'];
|
|
25
25
|
|
|
26
26
|
// Pre-init guard
|
|
27
27
|
if (!existsSync(join(planningDir, 'config.json'))) {
|
|
@@ -57,8 +57,10 @@ export function createCmdHealth(ctx) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// E3: templates/ missing
|
|
60
|
-
const templatesDir = join(planningDir, 'templates');
|
|
61
|
-
const
|
|
60
|
+
const templatesDir = join(planningDir, 'templates');
|
|
61
|
+
const runtimeHelpersDir = join(planningDir, 'bin');
|
|
62
|
+
const hasTemplatesDir = existsSync(templatesDir);
|
|
63
|
+
const hasRuntimeHelpersDir = existsSync(runtimeHelpersDir);
|
|
62
64
|
const rolesDir = join(templatesDir, 'roles');
|
|
63
65
|
const delegatesDir = join(templatesDir, 'delegates');
|
|
64
66
|
const hasRolesDir = hasTemplatesDir && existsSync(rolesDir);
|
|
@@ -121,32 +123,33 @@ export function createCmdHealth(ctx) {
|
|
|
121
123
|
// --- WARNING checks ---
|
|
122
124
|
|
|
123
125
|
// W1: generation-manifest.json missing
|
|
124
|
-
const manifest = skipInstalledTemplateChecks ? null : readManifest(planningDir);
|
|
125
|
-
if (!manifest && !skipInstalledTemplateChecks) {
|
|
126
|
-
warnings.push({ id: 'W1', severity: 'WARN', message: 'generation-manifest.json missing', fix: 'Run `gsdd update
|
|
127
|
-
}
|
|
126
|
+
const manifest = skipInstalledTemplateChecks ? null : readManifest(planningDir);
|
|
127
|
+
if (!manifest && !skipInstalledTemplateChecks) {
|
|
128
|
+
warnings.push({ id: 'W1', severity: 'WARN', message: 'generation-manifest.json missing', fix: 'Run `gsdd update` to create' });
|
|
129
|
+
}
|
|
128
130
|
|
|
129
131
|
// W2 + W3: template/role hash mismatches and missing files
|
|
130
132
|
if (manifest && hasTemplatesDir) {
|
|
131
|
-
const allCategories = [
|
|
132
|
-
{ name: 'delegates', dir: delegatesDir, hashes: hasDelegatesDir ? manifest.templates?.delegates : null },
|
|
133
|
-
{ name: 'research', dir: join(templatesDir, 'research'), hashes: manifest.templates?.research },
|
|
134
|
-
{ name: 'codebase', dir: join(templatesDir, 'codebase'), hashes: manifest.templates?.codebase },
|
|
135
|
-
{ name: 'root templates', dir: templatesDir, hashes: manifest.templates?.root },
|
|
136
|
-
{ name: 'roles', dir: rolesDir, hashes: hasRolesDir ? manifest.roles : null },
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
133
|
+
const allCategories = [
|
|
134
|
+
{ name: 'delegates', dir: delegatesDir, hashes: hasDelegatesDir ? manifest.templates?.delegates : null, fixCommand: 'gsdd update --templates' },
|
|
135
|
+
{ name: 'research', dir: join(templatesDir, 'research'), hashes: manifest.templates?.research, fixCommand: 'gsdd update --templates' },
|
|
136
|
+
{ name: 'codebase', dir: join(templatesDir, 'codebase'), hashes: manifest.templates?.codebase, fixCommand: 'gsdd update --templates' },
|
|
137
|
+
{ name: 'root templates', dir: templatesDir, hashes: manifest.templates?.root, fixCommand: 'gsdd update --templates' },
|
|
138
|
+
{ name: 'roles', dir: rolesDir, hashes: hasRolesDir ? manifest.roles : null, fixCommand: 'gsdd update --templates' },
|
|
139
|
+
{ name: 'runtime helpers', dir: planningDir, hashes: hasRuntimeHelpersDir ? manifest.runtimeHelpers : null, fixCommand: 'gsdd update' },
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
for (const cat of allCategories) {
|
|
143
|
+
if (!cat.hashes) continue;
|
|
144
|
+
const result = detectModifications(cat.dir, cat.hashes);
|
|
145
|
+
if (result.modified.length > 0) {
|
|
146
|
+
warnings.push({ id: 'W2', severity: 'WARN', message: `${cat.name}: ${result.modified.length} file(s) modified locally (${result.modified.join(', ')})`, fix: `Intentional? Run \`${cat.fixCommand}\` to reset` });
|
|
147
|
+
}
|
|
148
|
+
if (result.missing.length > 0) {
|
|
149
|
+
warnings.push({ id: 'W3', severity: 'WARN', message: `${cat.name}: ${result.missing.length} file(s) missing from disk (${result.missing.join(', ')})`, fix: `Run \`${cat.fixCommand}\` to restore` });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
150
153
|
|
|
151
154
|
// W4: ROADMAP.md references phases not found in .planning/phases/
|
|
152
155
|
const roadmapPath = join(planningDir, 'ROADMAP.md');
|
package/bin/lib/init-flow.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync } from 'fs';
|
|
2
|
-
import { join, isAbsolute } from 'path';
|
|
3
|
-
import { renderSkillContent } from './rendering.mjs';
|
|
4
|
-
import { buildManifest, writeManifest } from './manifest.mjs';
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, cpSync } from 'fs';
|
|
2
|
+
import { dirname, join, isAbsolute } from 'path';
|
|
3
|
+
import { buildPlanningCliHelperEntries, renderSkillContent } from './rendering.mjs';
|
|
4
|
+
import { buildManifest, readManifest, writeManifest } from './manifest.mjs';
|
|
5
5
|
import { parseFlagValue, parseToolsFlag, parseAutoFlag } from './cli-utils.mjs';
|
|
6
6
|
import { buildDefaultConfig, COST_PROFILES, RIGOR_PROFILES } from './models.mjs';
|
|
7
7
|
import { installProjectTemplates, refreshTemplates } from './templates.mjs';
|
|
@@ -106,8 +106,11 @@ export function createCmdInit(ctx) {
|
|
|
106
106
|
console.log(' - copied project brief to .planning/PROJECT_BRIEF.md');
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
generateOpenStandardSkills(ctx.cwd, ctx.workflows);
|
|
110
|
-
console.log(' - generated open-standard skills (.agents/skills/gsdd-*)');
|
|
109
|
+
generateOpenStandardSkills(ctx.cwd, ctx.workflows);
|
|
110
|
+
console.log(' - generated open-standard skills (.agents/skills/gsdd-*)');
|
|
111
|
+
|
|
112
|
+
generatePlanningCliHelpers(ctx);
|
|
113
|
+
console.log(' - generated local workflow helpers (.planning/bin/gsdd*)');
|
|
111
114
|
|
|
112
115
|
for (const adapter of resolveAdapters(ctx.adapters, interactiveSession.adapterTargets)) {
|
|
113
116
|
adapter.generate();
|
|
@@ -144,15 +147,25 @@ export function createCmdUpdate(ctx) {
|
|
|
144
147
|
updated = true;
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
if (platforms.length > 0 || existsSync(join(ctx.cwd, '.agents', 'skills'))) {
|
|
148
|
-
if (isDry) {
|
|
149
|
-
console.log(' - would update open-standard skills (.agents/skills/gsdd-*)');
|
|
150
|
-
} else {
|
|
151
|
-
generateOpenStandardSkills(ctx.cwd, ctx.workflows);
|
|
150
|
+
if (platforms.length > 0 || existsSync(join(ctx.cwd, '.agents', 'skills'))) {
|
|
151
|
+
if (isDry) {
|
|
152
|
+
console.log(' - would update open-standard skills (.agents/skills/gsdd-*)');
|
|
153
|
+
} else {
|
|
154
|
+
generateOpenStandardSkills(ctx.cwd, ctx.workflows);
|
|
152
155
|
console.log(' - updated open-standard skills (.agents/skills/gsdd-*)');
|
|
153
|
-
}
|
|
154
|
-
updated = true;
|
|
155
|
-
}
|
|
156
|
+
}
|
|
157
|
+
updated = true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (existsSync(ctx.planningDir)) {
|
|
161
|
+
if (isDry) {
|
|
162
|
+
console.log(' - would update local workflow helpers (.planning/bin/gsdd*)');
|
|
163
|
+
} else {
|
|
164
|
+
generatePlanningCliHelpers(ctx);
|
|
165
|
+
console.log(' - updated local workflow helpers (.planning/bin/gsdd*)');
|
|
166
|
+
}
|
|
167
|
+
updated = true;
|
|
168
|
+
}
|
|
156
169
|
|
|
157
170
|
for (const adapter of getAdaptersToUpdate(ctx.adapters, platforms)) {
|
|
158
171
|
if (isDry) {
|
|
@@ -167,26 +180,72 @@ export function createCmdUpdate(ctx) {
|
|
|
167
180
|
|
|
168
181
|
if (!updated) {
|
|
169
182
|
console.log(' - no adapters found to update (run `gsdd init` first)');
|
|
170
|
-
} else if (isDry) {
|
|
171
|
-
console.log('\nDry run complete. No files were written.\n');
|
|
172
|
-
} else {
|
|
173
|
-
if (
|
|
174
|
-
const manifest =
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
183
|
+
} else if (isDry) {
|
|
184
|
+
console.log('\nDry run complete. No files were written.\n');
|
|
185
|
+
} else {
|
|
186
|
+
if (existsSync(ctx.planningDir)) {
|
|
187
|
+
const manifest = buildUpdateManifest({
|
|
188
|
+
planningDir: ctx.planningDir,
|
|
189
|
+
frameworkVersion: ctx.frameworkVersion,
|
|
190
|
+
updateTemplates: doTemplates,
|
|
191
|
+
});
|
|
192
|
+
if (manifest) {
|
|
193
|
+
writeManifest(ctx.planningDir, manifest);
|
|
194
|
+
console.log(' - updated generation manifest');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
console.log('\nAdapters updated.\n');
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function generateOpenStandardSkills(cwd, workflows) {
|
|
203
|
+
for (const workflow of workflows) {
|
|
204
|
+
const dir = join(cwd, '.agents', 'skills', workflow.name);
|
|
205
|
+
mkdirSync(dir, { recursive: true });
|
|
206
|
+
writeFileSync(join(dir, 'SKILL.md'), renderSkillContent(workflow));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function generatePlanningCliHelpers(ctx) {
|
|
211
|
+
for (const entry of buildPlanningCliHelperEntries({
|
|
212
|
+
packageName: ctx.packageName,
|
|
213
|
+
packageVersion: ctx.packageVersion,
|
|
214
|
+
})) {
|
|
215
|
+
const absolutePath = join(ctx.planningDir, entry.relativePath);
|
|
216
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
217
|
+
writeFileSync(absolutePath, entry.content);
|
|
218
|
+
if (!absolutePath.endsWith('.cmd')) {
|
|
219
|
+
chmodSync(absolutePath, 0o755);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildUpdateManifest({ planningDir, frameworkVersion, updateTemplates }) {
|
|
225
|
+
const existingManifest = readManifest(planningDir);
|
|
226
|
+
const nextManifest = buildManifest({ planningDir, frameworkVersion });
|
|
227
|
+
|
|
228
|
+
if (existingManifest && !updateTemplates) {
|
|
229
|
+
nextManifest.templates = existingManifest.templates ?? nextManifest.templates;
|
|
230
|
+
nextManifest.roles = existingManifest.roles ?? nextManifest.roles;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (existingManifest && manifestsEqualIgnoringTimestamp(existingManifest, nextManifest)) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return nextManifest;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function manifestsEqualIgnoringTimestamp(left, right) {
|
|
241
|
+
return JSON.stringify(stripManifestTimestamp(left)) === JSON.stringify(stripManifestTimestamp(right));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function stripManifestTimestamp(manifest) {
|
|
245
|
+
if (!manifest || typeof manifest !== 'object') return manifest;
|
|
246
|
+
const { generatedAt, ...rest } = manifest;
|
|
247
|
+
return rest;
|
|
248
|
+
}
|
|
190
249
|
|
|
191
250
|
async function ensureConfig({ cwd, planningDir, isAuto, promptApi, preselectedConfig = null }) {
|
|
192
251
|
const configFile = join(planningDir, 'config.json');
|
package/bin/lib/init-runtime.mjs
CHANGED
|
@@ -161,8 +161,8 @@ export function getPostInitRoutingLines(selectedRuntimes) {
|
|
|
161
161
|
|
|
162
162
|
export function getHelpText() {
|
|
163
163
|
return `
|
|
164
|
-
gsdd - Workspine CLI
|
|
165
|
-
|
|
164
|
+
gsdd - Workspine CLI
|
|
165
|
+
Repo-native delivery spine for long-horizon AI-assisted work across coding runtimes.
|
|
166
166
|
|
|
167
167
|
Usage: gsdd <command> [args]
|
|
168
168
|
|
|
@@ -198,8 +198,9 @@ Platforms (for --tools):
|
|
|
198
198
|
all Generate all adapters (Claude, OpenCode, Codex, AGENTS.md, Cursor, Copilot, Gemini)
|
|
199
199
|
|
|
200
200
|
Notes:
|
|
201
|
-
- init always generates open-standard skills at .agents/skills/gsdd-* (portable workflow entrypoints)
|
|
202
|
-
-
|
|
201
|
+
- init always generates open-standard skills at .agents/skills/gsdd-* (portable workflow entrypoints)
|
|
202
|
+
- init also generates a local .planning/bin/gsdd* helper surface so workflow-embedded helper commands do not depend on a global gsdd binary
|
|
203
|
+
- Workspine is the public product name; the retained package, command, workflow, and workspace contracts stay gsdd-cli, gsdd, gsdd-*, and .planning/
|
|
203
204
|
- running plain \`gsdd init\` in a terminal opens the guided runtime-selection wizard
|
|
204
205
|
- the wizard lets you pick runtimes first, then separately decide whether repo-wide AGENTS.md governance is worth installing
|
|
205
206
|
- \`gsdd health\` compares any installed generated runtime surfaces against current render output and points back to \`gsdd update\` when they drift
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const BROWNFIELD_CHANGE_DIR = 'brownfield-change';
|
|
3
5
|
|
|
4
6
|
const PHASE_LINE_RE = /^\s*[-*]\s*\[([ x-])\]\s*\*\*Phase\s+(\d+(?:\.\d+)*[a-z]?):\s*(.+?)\*\*(?:\s+—\s+\[([^\]]+)])?/i;
|
|
5
7
|
const ACTIVE_MILESTONE_HEADING_RE = /^###\s+(v[^\s]+)\s+(.+)$/im;
|
|
@@ -15,9 +17,16 @@ export function evaluateLifecycleState({ planningDir, provenance = null } = {})
|
|
|
15
17
|
const milestonesPath = join(planningDir, 'MILESTONES.md');
|
|
16
18
|
const phasesDir = join(planningDir, 'phases');
|
|
17
19
|
|
|
18
|
-
const spec = readTextIfExists(specPath);
|
|
19
|
-
const roadmap = readTextIfExists(roadmapPath);
|
|
20
|
-
const milestones = readTextIfExists(milestonesPath);
|
|
20
|
+
const spec = readTextIfExists(specPath);
|
|
21
|
+
const roadmap = readTextIfExists(roadmapPath);
|
|
22
|
+
const milestones = readTextIfExists(milestonesPath);
|
|
23
|
+
const brownfieldChange = readBrownfieldChangeState(planningDir);
|
|
24
|
+
const nonPhaseState = deriveNonPhaseState({
|
|
25
|
+
planningDir,
|
|
26
|
+
hasSpec: Boolean(spec.trim()),
|
|
27
|
+
hasRoadmap: Boolean(roadmap.trim()),
|
|
28
|
+
brownfieldChange,
|
|
29
|
+
});
|
|
21
30
|
|
|
22
31
|
const phases = parseActiveRoadmapPhases(roadmap);
|
|
23
32
|
const phaseArtifacts = collectPhaseArtifacts(phasesDir);
|
|
@@ -69,10 +78,12 @@ export function evaluateLifecycleState({ planningDir, provenance = null } = {})
|
|
|
69
78
|
phases: enrichedPhases,
|
|
70
79
|
currentPhase: enrichedPhases.find((phase) => phase.status === 'in_progress') || null,
|
|
71
80
|
nextPhase: enrichedPhases.find((phase) => phase.status === 'not_started') || null,
|
|
72
|
-
counts,
|
|
73
|
-
phaseArtifacts,
|
|
74
|
-
incompletePlans,
|
|
75
|
-
|
|
81
|
+
counts,
|
|
82
|
+
phaseArtifacts,
|
|
83
|
+
incompletePlans,
|
|
84
|
+
brownfieldChange,
|
|
85
|
+
nonPhaseState,
|
|
86
|
+
requirementAlignment: evaluateRequirementAlignment(spec, enrichedPhases),
|
|
76
87
|
provenance: provenance
|
|
77
88
|
? {
|
|
78
89
|
provided: true,
|
|
@@ -86,9 +97,13 @@ function readTextIfExists(filePath) {
|
|
|
86
97
|
return existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
|
|
87
98
|
}
|
|
88
99
|
|
|
89
|
-
function normalizeContent(content) {
|
|
90
|
-
return String(content || '').replace(/\r\n/g, '\n');
|
|
91
|
-
}
|
|
100
|
+
function normalizeContent(content) {
|
|
101
|
+
return String(content || '').replace(/\r\n/g, '\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function escapeRegExp(value) {
|
|
105
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
106
|
+
}
|
|
92
107
|
|
|
93
108
|
export function normalizePhaseToken(value) {
|
|
94
109
|
const raw = String(value || '').trim().toLowerCase();
|
|
@@ -108,8 +123,8 @@ function normalizePhaseStatus(marker) {
|
|
|
108
123
|
return 'not_started';
|
|
109
124
|
}
|
|
110
125
|
|
|
111
|
-
function parseActiveRoadmapPhases(roadmap) {
|
|
112
|
-
if (!roadmap) return [];
|
|
126
|
+
function parseActiveRoadmapPhases(roadmap) {
|
|
127
|
+
if (!roadmap) return [];
|
|
113
128
|
|
|
114
129
|
const phases = [];
|
|
115
130
|
let inDetails = false;
|
|
@@ -135,12 +150,74 @@ function parseActiveRoadmapPhases(roadmap) {
|
|
|
135
150
|
requirements: splitRequirementList(match[4]),
|
|
136
151
|
});
|
|
137
152
|
}
|
|
138
|
-
|
|
139
|
-
return phases;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function
|
|
143
|
-
|
|
153
|
+
|
|
154
|
+
return phases;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function readBrownfieldChangeState(planningDir) {
|
|
158
|
+
const dir = join(planningDir, BROWNFIELD_CHANGE_DIR);
|
|
159
|
+
const changePath = join(dir, 'CHANGE.md');
|
|
160
|
+
const handoffPath = join(dir, 'HANDOFF.md');
|
|
161
|
+
|
|
162
|
+
if (!existsSync(changePath)) {
|
|
163
|
+
return {
|
|
164
|
+
exists: false,
|
|
165
|
+
dir,
|
|
166
|
+
changePath,
|
|
167
|
+
handoffPath,
|
|
168
|
+
title: null,
|
|
169
|
+
changeId: null,
|
|
170
|
+
currentStatus: null,
|
|
171
|
+
currentIntegrationSurface: null,
|
|
172
|
+
currentOwnerRuntime: null,
|
|
173
|
+
nextAction: null,
|
|
174
|
+
declaredOwnedPaths: [],
|
|
175
|
+
handoff: null,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const changeArtifact = readMarkdownArtifact(changePath);
|
|
180
|
+
const handoffArtifact = existsSync(handoffPath) ? readMarkdownArtifact(handoffPath) : null;
|
|
181
|
+
const currentStatusSection = extractMarkdownSection(changeArtifact.body, 'Current Status');
|
|
182
|
+
const nextActionSection = extractMarkdownSection(changeArtifact.body, 'Next Action');
|
|
183
|
+
const sliceSection = extractMarkdownSection(changeArtifact.body, 'PR Slice Ownership');
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
exists: true,
|
|
187
|
+
dir,
|
|
188
|
+
changePath,
|
|
189
|
+
handoffPath,
|
|
190
|
+
title: extractMarkdownHeading(changeArtifact.body),
|
|
191
|
+
changeId: changeArtifact.frontmatter.change || null,
|
|
192
|
+
currentStatus: changeArtifact.frontmatter.status || extractBulletLabel(currentStatusSection, 'Current posture'),
|
|
193
|
+
currentIntegrationSurface: extractBulletLabel(currentStatusSection, 'Current branch / integration surface'),
|
|
194
|
+
currentOwnerRuntime: extractBulletLabel(currentStatusSection, 'Current owner / runtime'),
|
|
195
|
+
nextAction: collapseMarkdownSection(nextActionSection),
|
|
196
|
+
declaredOwnedPaths: parseOwnedPathHints(sliceSection),
|
|
197
|
+
handoff: handoffArtifact
|
|
198
|
+
? {
|
|
199
|
+
updated: handoffArtifact.frontmatter.updated || null,
|
|
200
|
+
activeConstraints: collapseMarkdownSection(extractMarkdownSection(handoffArtifact.body, 'Active Constraints')),
|
|
201
|
+
unresolvedUncertainty: collapseMarkdownSection(extractMarkdownSection(handoffArtifact.body, 'Unresolved Uncertainty')),
|
|
202
|
+
decisionPosture: collapseMarkdownSection(extractMarkdownSection(handoffArtifact.body, 'Decision Posture')),
|
|
203
|
+
antiRegression: collapseMarkdownSection(extractMarkdownSection(handoffArtifact.body, 'Anti-Regression')),
|
|
204
|
+
nextActionContext: collapseMarkdownSection(extractMarkdownSection(handoffArtifact.body, 'Next Action')),
|
|
205
|
+
}
|
|
206
|
+
: null,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function deriveNonPhaseState({ planningDir, hasSpec, hasRoadmap, brownfieldChange } = {}) {
|
|
211
|
+
if (brownfieldChange?.exists) return 'active_brownfield_change';
|
|
212
|
+
if (hasRoadmap) return null;
|
|
213
|
+
if (hasSpec) return 'between_milestones';
|
|
214
|
+
if (hasSubstantiveCodebaseMaps(planningDir)) return 'codebase_only';
|
|
215
|
+
if (hasQuickLaneArtifacts(planningDir)) return 'quick_lane';
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function splitRequirementList(rawRequirements = '') {
|
|
220
|
+
return String(rawRequirements)
|
|
144
221
|
.split(',')
|
|
145
222
|
.map((entry) => entry.trim())
|
|
146
223
|
.filter(Boolean);
|
|
@@ -262,10 +339,10 @@ function deriveCurrentMilestone({ roadmap, planningDir, milestoneLedger, counts
|
|
|
262
339
|
};
|
|
263
340
|
}
|
|
264
341
|
|
|
265
|
-
function evaluateRequirementAlignment(spec, phases) {
|
|
266
|
-
const checkedRequirements = new Set(
|
|
267
|
-
[...normalizeContent(spec).matchAll(/- \[x\] \*\*\[([A-Z0-9-]+)]\*\*/g)].map((result) => result[1])
|
|
268
|
-
);
|
|
342
|
+
function evaluateRequirementAlignment(spec, phases) {
|
|
343
|
+
const checkedRequirements = new Set(
|
|
344
|
+
[...normalizeContent(spec).matchAll(/- \[x\] \*\*\[([A-Z0-9-]+)]\*\*/g)].map((result) => result[1])
|
|
345
|
+
);
|
|
269
346
|
|
|
270
347
|
const roadmapRequirements = new Map();
|
|
271
348
|
for (const phase of phases) {
|
|
@@ -287,7 +364,113 @@ function evaluateRequirementAlignment(spec, phases) {
|
|
|
287
364
|
|
|
288
365
|
return {
|
|
289
366
|
checkedRequirements,
|
|
290
|
-
roadmapRequirements,
|
|
291
|
-
mismatches,
|
|
292
|
-
};
|
|
293
|
-
}
|
|
367
|
+
roadmapRequirements,
|
|
368
|
+
mismatches,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function readMarkdownArtifact(filePath) {
|
|
373
|
+
const raw = readTextIfExists(filePath);
|
|
374
|
+
const normalized = normalizeContent(raw);
|
|
375
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
376
|
+
if (!match) {
|
|
377
|
+
return {
|
|
378
|
+
raw: normalized,
|
|
379
|
+
frontmatter: {},
|
|
380
|
+
body: normalized,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
raw: normalized,
|
|
386
|
+
frontmatter: parseFrontmatter(match[1]),
|
|
387
|
+
body: normalized.slice(match[0].length),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function parseFrontmatter(content) {
|
|
392
|
+
const data = {};
|
|
393
|
+
for (const line of normalizeContent(content).split('\n')) {
|
|
394
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.+)$/);
|
|
395
|
+
if (!match) continue;
|
|
396
|
+
data[match[1]] = match[2].trim();
|
|
397
|
+
}
|
|
398
|
+
return data;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function extractMarkdownHeading(content) {
|
|
402
|
+
return normalizeContent(content).match(/^#\s+(.+)$/m)?.[1]?.trim() || null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function extractMarkdownSection(content, heading) {
|
|
406
|
+
const normalized = normalizeContent(content);
|
|
407
|
+
const headingMatch = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, 'm').exec(normalized);
|
|
408
|
+
if (!headingMatch) return '';
|
|
409
|
+
|
|
410
|
+
const start = headingMatch.index + headingMatch[0].length;
|
|
411
|
+
const remainder = normalized.slice(start).replace(/^\n/, '');
|
|
412
|
+
const nextHeading = /\n##\s+/.exec(remainder);
|
|
413
|
+
const end = nextHeading ? nextHeading.index : remainder.length;
|
|
414
|
+
return remainder.slice(0, end).trim();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function extractBulletLabel(section, label) {
|
|
418
|
+
const match = normalizeContent(section).match(
|
|
419
|
+
new RegExp(`^[-*]\\s+${escapeRegExp(label)}\\s*:\\s*(.+)$`, 'im')
|
|
420
|
+
);
|
|
421
|
+
return match ? match[1].trim() : null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function collapseMarkdownSection(section) {
|
|
425
|
+
return normalizeContent(section)
|
|
426
|
+
.split('\n')
|
|
427
|
+
.map((line) => line.replace(/^[-*]\s+/, '').trim())
|
|
428
|
+
.filter(Boolean)
|
|
429
|
+
.join(' ');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function parseOwnedPathHints(section) {
|
|
433
|
+
const lines = normalizeContent(section)
|
|
434
|
+
.split('\n')
|
|
435
|
+
.map((line) => line.trim())
|
|
436
|
+
.filter((line) => line.startsWith('|'));
|
|
437
|
+
const paths = [];
|
|
438
|
+
|
|
439
|
+
for (const line of lines.slice(2)) {
|
|
440
|
+
const columns = line.split('|').map((column) => column.trim());
|
|
441
|
+
const owned = columns[3];
|
|
442
|
+
if (!owned) continue;
|
|
443
|
+
|
|
444
|
+
for (const candidate of owned.split(',')) {
|
|
445
|
+
const normalized = normalizeOwnedPathHint(candidate);
|
|
446
|
+
if (normalized) paths.push(normalized);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return [...new Set(paths)];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function normalizeOwnedPathHint(value) {
|
|
454
|
+
const normalized = String(value || '')
|
|
455
|
+
.replace(/[`[\]]/g, '')
|
|
456
|
+
.trim()
|
|
457
|
+
.replace(/\\/g, '/');
|
|
458
|
+
if (!normalized) return null;
|
|
459
|
+
if (/^(disjoint write set|owned files \/ modules|what this slice does|planned)$/i.test(normalized)) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
if (!/[/.\\*]/.test(normalized)) return null;
|
|
463
|
+
return normalized;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function hasSubstantiveCodebaseMaps(planningDir) {
|
|
467
|
+
const dir = join(planningDir, 'codebase');
|
|
468
|
+
if (!existsSync(dir)) return false;
|
|
469
|
+
return readdirSync(dir).some((entry) => entry.toLowerCase().endsWith('.md'));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function hasQuickLaneArtifacts(planningDir) {
|
|
473
|
+
const dir = join(planningDir, 'quick');
|
|
474
|
+
if (!existsSync(dir)) return false;
|
|
475
|
+
return readdirSync(dir).length > 0;
|
|
476
|
+
}
|