gsdd-cli 0.27.0 → 0.29.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/README.md +53 -22
- package/agents/DISTILLATION.md +2 -2
- package/agents/README.md +4 -4
- package/agents/approach-explorer.md +4 -4
- package/agents/executor.md +20 -20
- package/agents/integration-checker.md +2 -2
- package/agents/planner.md +10 -26
- package/agents/researcher.md +2 -2
- package/agents/roadmapper.md +6 -6
- package/agents/synthesizer.md +18 -18
- package/agents/verifier.md +3 -3
- package/bin/adapters/agents.mjs +3 -3
- package/bin/adapters/claude.mjs +16 -14
- package/bin/adapters/opencode.mjs +15 -12
- package/bin/gsdd.mjs +16 -13
- package/bin/lib/{models.mjs → config.mjs} +23 -16
- package/bin/lib/control-map.mjs +17 -488
- package/bin/lib/health-truth.mjs +8 -13
- package/bin/lib/health.mjs +25 -39
- package/bin/lib/init-flow.mjs +44 -38
- package/bin/lib/init-prompts.mjs +3 -3
- package/bin/lib/init-runtime.mjs +11 -30
- package/bin/lib/lifecycle-preflight.mjs +97 -410
- package/bin/lib/lifecycle-state.mjs +2 -1
- package/bin/lib/next.mjs +243 -20
- package/bin/lib/phase.mjs +706 -280
- package/bin/lib/plan-constants.mjs +0 -5
- package/bin/lib/rendering.mjs +64 -44
- package/bin/lib/runtime-freshness.mjs +18 -15
- package/bin/lib/state-dir.mjs +45 -0
- package/bin/lib/templates.mjs +59 -22
- package/bin/lib/work-context.mjs +12 -1
- package/bin/lib/workflows.mjs +0 -1
- package/bin/lib/workspace-root.mjs +11 -6
- package/distilled/DESIGN.md +89 -31
- package/distilled/EVIDENCE-INDEX.md +18 -5
- package/distilled/README.md +23 -33
- package/distilled/SKILL.md +9 -10
- package/distilled/templates/agents.block.md +5 -5
- package/distilled/templates/approach.md +3 -3
- package/distilled/templates/auth-matrix.md +2 -2
- package/distilled/templates/brownfield-change/CHANGE.md +1 -1
- package/distilled/templates/delegates/approach-explorer.md +5 -5
- package/distilled/templates/delegates/mapper-arch.md +3 -3
- package/distilled/templates/delegates/mapper-concerns.md +4 -4
- package/distilled/templates/delegates/mapper-quality.md +3 -3
- package/distilled/templates/delegates/mapper-tech.md +3 -3
- package/distilled/templates/delegates/plan-checker.md +18 -19
- package/distilled/templates/delegates/researcher-architecture.md +3 -3
- package/distilled/templates/delegates/researcher-features.md +3 -3
- package/distilled/templates/delegates/researcher-pitfalls.md +3 -3
- package/distilled/templates/delegates/researcher-stack.md +3 -3
- package/distilled/templates/delegates/researcher-synthesizer.md +7 -7
- package/distilled/templates/research/architecture.md +1 -1
- package/distilled/templates/research/pitfalls.md +1 -1
- package/distilled/templates/research/stack.md +1 -1
- package/distilled/templates/roadmap.md +4 -4
- package/distilled/templates/spec.md +2 -2
- package/distilled/templates/ui-proof.md +81 -181
- package/distilled/workflows/audit-milestone.md +23 -23
- package/distilled/workflows/complete-milestone.md +35 -35
- package/distilled/workflows/execute.md +34 -35
- package/distilled/workflows/map-codebase.md +30 -30
- package/distilled/workflows/new-milestone.md +18 -18
- package/distilled/workflows/new-project.md +45 -45
- package/distilled/workflows/pause.md +15 -15
- package/distilled/workflows/plan.md +106 -114
- package/distilled/workflows/progress.md +40 -39
- package/distilled/workflows/quick.md +49 -50
- package/distilled/workflows/resume.md +23 -22
- package/distilled/workflows/verify-work.md +7 -7
- package/distilled/workflows/verify.md +26 -26
- package/docs/BROWNFIELD-PROOF.md +1 -1
- package/docs/RUNTIME-SUPPORT.md +13 -13
- package/docs/USER-GUIDE.md +26 -21
- package/docs/claude/context-monitor.md +1 -1
- package/docs/proof/consumer-node-cli/README.md +1 -1
- package/package.json +3 -3
- package/bin/lib/closeout-report.mjs +0 -318
- package/bin/lib/evidence-contract.mjs +0 -325
- package/bin/lib/provenance.mjs +0 -390
- package/bin/lib/session-fingerprint.mjs +0 -223
- package/bin/lib/ui-proof.mjs +0 -1007
- package/distilled/workflows/plan-milestone-gaps.md +0 -204
|
@@ -7,11 +7,6 @@ export const PLAN_CHECK_DIMENSIONS = [
|
|
|
7
7
|
'must_have_quality',
|
|
8
8
|
'context_compliance',
|
|
9
9
|
'goal_achievement',
|
|
10
|
-
'scope_boundaries',
|
|
11
|
-
'anti_regression_capture',
|
|
12
|
-
'escalation_integrity',
|
|
13
|
-
'closure_honesty',
|
|
14
|
-
'high_leverage_review',
|
|
15
10
|
'approach_alignment',
|
|
16
11
|
];
|
|
17
12
|
|
package/bin/lib/rendering.mjs
CHANGED
|
@@ -7,17 +7,27 @@ const __dirname = dirname(__filename);
|
|
|
7
7
|
const DISTILLED_DIR = join(__dirname, '..', '..', 'distilled');
|
|
8
8
|
const HELPER_LIB_FILES = Object.freeze([
|
|
9
9
|
'cli-utils.mjs',
|
|
10
|
-
'closeout-report.mjs',
|
|
11
10
|
'control-map.mjs',
|
|
12
|
-
'evidence-contract.mjs',
|
|
13
11
|
'file-ops.mjs',
|
|
14
12
|
'lifecycle-preflight.mjs',
|
|
15
13
|
'lifecycle-state.mjs',
|
|
14
|
+
'next.mjs',
|
|
16
15
|
'phase.mjs',
|
|
17
|
-
'
|
|
18
|
-
'
|
|
16
|
+
'state-dir.mjs',
|
|
17
|
+
'work-context.mjs',
|
|
19
18
|
'workspace-root.mjs',
|
|
20
19
|
]);
|
|
20
|
+
const DEFAULT_STATE_DIR_NAME = '.work';
|
|
21
|
+
|
|
22
|
+
function normalizeStateDirName(stateDirName = DEFAULT_STATE_DIR_NAME) {
|
|
23
|
+
return stateDirName || DEFAULT_STATE_DIR_NAME;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function localizeStateDirReferences(content, { stateDirName = DEFAULT_STATE_DIR_NAME } = {}) {
|
|
27
|
+
const normalized = normalizeStateDirName(stateDirName);
|
|
28
|
+
if (normalized === DEFAULT_STATE_DIR_NAME) return content;
|
|
29
|
+
return String(content).replace(/\.work(?=\/|\\|`|'|"|\)|\]|\}|,|\.|:|;|\s|$)/g, normalized);
|
|
30
|
+
}
|
|
21
31
|
|
|
22
32
|
function getWorkflowContent(workflowFile) {
|
|
23
33
|
const filePath = join(DISTILLED_DIR, 'workflows', workflowFile);
|
|
@@ -31,8 +41,8 @@ function getDelegateContent(delegateFile) {
|
|
|
31
41
|
return `<!-- Delegate file not found: ${delegateFile} -->\n`;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
function renderSkillContent(workflow) {
|
|
35
|
-
const workflowContent = getWorkflowContent(workflow.workflow);
|
|
44
|
+
function renderSkillContent(workflow, options = {}) {
|
|
45
|
+
const workflowContent = localizeStateDirReferences(getWorkflowContent(workflow.workflow), options);
|
|
36
46
|
return `---
|
|
37
47
|
name: ${workflow.name}
|
|
38
48
|
description: ${workflow.description}
|
|
@@ -43,60 +53,63 @@ agent: ${workflow.agent}
|
|
|
43
53
|
${workflowContent}`;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
function renderPlanningCliLauncher() {
|
|
56
|
+
function renderPlanningCliLauncher({ stateDirName = DEFAULT_STATE_DIR_NAME } = {}) {
|
|
57
|
+
const helperPath = `${normalizeStateDirName(stateDirName)}/bin/gsdd.mjs`;
|
|
58
|
+
const checkpointBackupPath = `${normalizeStateDirName(stateDirName)}/.continue-here.bak`;
|
|
47
59
|
return `#!/usr/bin/env node
|
|
48
60
|
|
|
49
61
|
import { cmdFileOp } from './lib/file-ops.mjs';
|
|
50
62
|
import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
|
|
51
63
|
import { cmdPhaseStatus, cmdVerify } from './lib/phase.mjs';
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import { cmdControlMap } from './lib/control-map.mjs';
|
|
55
|
-
import { createCmdCloseoutReport } from './lib/closeout-report.mjs';
|
|
64
|
+
import { buildControlMap } from './lib/control-map.mjs';
|
|
65
|
+
import { createCmdNext } from './lib/next.mjs';
|
|
56
66
|
import { bootstrapHelperWorkspace, consumeWorkspaceRootArg, resolveWorkspaceContext } from './lib/workspace-root.mjs';
|
|
57
67
|
|
|
58
68
|
const HELPER_CONTEXT = {
|
|
69
|
+
cwd: process.cwd(),
|
|
59
70
|
workflows: [],
|
|
60
71
|
frameworkVersion: 'generated-helper',
|
|
61
72
|
};
|
|
62
|
-
const
|
|
73
|
+
const cmdNext = createCmdNext(HELPER_CONTEXT);
|
|
74
|
+
|
|
75
|
+
function cmdControlMap(...controlArgs) {
|
|
76
|
+
const context = resolveWorkspaceContext([], { cwd: HELPER_CONTEXT.cwd });
|
|
77
|
+
const report = buildControlMap({
|
|
78
|
+
workspaceRoot: context.workspaceRoot,
|
|
79
|
+
planningDir: context.planningDir,
|
|
80
|
+
includeIgnoredPaths: controlArgs.includes('--with-ignored'),
|
|
81
|
+
});
|
|
82
|
+
console.log(JSON.stringify(report, null, 2));
|
|
83
|
+
}
|
|
63
84
|
|
|
64
85
|
const COMMANDS = {
|
|
86
|
+
'control-map': cmdControlMap,
|
|
65
87
|
'file-op': cmdFileOp,
|
|
66
88
|
'lifecycle-preflight': cmdLifecyclePreflight,
|
|
67
89
|
'phase-status': cmdPhaseStatus,
|
|
68
90
|
verify: cmdVerify,
|
|
69
|
-
|
|
70
|
-
'ui-proof': cmdUiProof,
|
|
71
|
-
'control-map': cmdControlMap,
|
|
72
|
-
'closeout-report': cmdCloseoutReport,
|
|
91
|
+
next: cmdNext,
|
|
73
92
|
};
|
|
74
93
|
|
|
75
94
|
function printHelp() {
|
|
76
95
|
console.log([
|
|
77
|
-
'Usage: node
|
|
96
|
+
'Usage: node ${helperPath} [--workspace-root <path>] <command> [args]',
|
|
78
97
|
'',
|
|
79
98
|
'Local workflow helper commands:',
|
|
99
|
+
' control-map [--json] [--with-ignored]',
|
|
100
|
+
' Print computed repo/worktree/workflow state for workflow-internal checks',
|
|
80
101
|
' file-op <copy|delete|regex-sub>',
|
|
81
102
|
' Run deterministic workspace-confined file operations',
|
|
82
|
-
' Example: node
|
|
103
|
+
' Example: node ${helperPath} file-op delete ${checkpointBackupPath} --missing ok',
|
|
83
104
|
' phase-status <N> <status> Update ROADMAP.md phase status ([ ] / [-] / [x])',
|
|
84
|
-
' Example: node
|
|
85
|
-
' verify <N> Run direct phase artifact
|
|
86
|
-
' Example: node
|
|
105
|
+
' Example: node ${helperPath} phase-status 1 done',
|
|
106
|
+
' verify <N> Run direct phase artifact checks',
|
|
107
|
+
' Example: node ${helperPath} verify 1',
|
|
87
108
|
' lifecycle-preflight <surface> [phase]',
|
|
88
109
|
' Inspect lifecycle gate results for a workflow surface',
|
|
89
|
-
' Example: node
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
' ui-proof validate <path> [--claim <public|publication|tracked|delivery|release>]',
|
|
93
|
-
' Validate UI proof metadata; use --claim for stronger proof uses',
|
|
94
|
-
' ui-proof compare <planned-slots-json> [observed-bundle-json ...]',
|
|
95
|
-
' Compare planned UI proof slots against observed bundles',
|
|
96
|
-
' control-map [--json] [--with-ignored] [--annotations <path>]',
|
|
97
|
-
' Report computed repo/worktree/planning state and local annotations',
|
|
98
|
-
' closeout-report [--json] [--phase <N>]',
|
|
99
|
-
' Replay read-only closeout status from control-map, health, preflight, verify, and UI-proof signals',
|
|
110
|
+
' Example: node ${helperPath} lifecycle-preflight verify 1 --expects-mutation phase-status',
|
|
111
|
+
' next [--json] [--init]',
|
|
112
|
+
' Route to the next safe Workspine action from ${normalizeStateDirName(stateDirName)}, brownfield, planning, and repo truth',
|
|
100
113
|
'',
|
|
101
114
|
'Advanced option:',
|
|
102
115
|
' --workspace-root <path> Override workspace root discovery before or after the subcommand',
|
|
@@ -106,6 +119,7 @@ function printHelp() {
|
|
|
106
119
|
function applyWorkspaceRootOverride(workspaceRootArg) {
|
|
107
120
|
if (!workspaceRootArg) {
|
|
108
121
|
bootstrapHelperWorkspace(import.meta.url);
|
|
122
|
+
HELPER_CONTEXT.cwd = process.cwd();
|
|
109
123
|
return true;
|
|
110
124
|
}
|
|
111
125
|
|
|
@@ -117,6 +131,7 @@ function applyWorkspaceRootOverride(workspaceRootArg) {
|
|
|
117
131
|
}
|
|
118
132
|
|
|
119
133
|
process.env.GSDD_WORKSPACE_ROOT = context.workspaceRoot;
|
|
134
|
+
HELPER_CONTEXT.cwd = context.workspaceRoot;
|
|
120
135
|
try {
|
|
121
136
|
process.chdir(context.workspaceRoot);
|
|
122
137
|
} catch {
|
|
@@ -183,11 +198,11 @@ function readHelperLibContent(fileName) {
|
|
|
183
198
|
return readFileSync(join(__dirname, fileName), 'utf-8');
|
|
184
199
|
}
|
|
185
200
|
|
|
186
|
-
function buildPlanningCliHelperEntries() {
|
|
201
|
+
function buildPlanningCliHelperEntries(options = {}) {
|
|
187
202
|
return [
|
|
188
203
|
{
|
|
189
204
|
relativePath: 'bin/gsdd.mjs',
|
|
190
|
-
content: renderPlanningCliLauncher(),
|
|
205
|
+
content: renderPlanningCliLauncher(options),
|
|
191
206
|
},
|
|
192
207
|
{
|
|
193
208
|
relativePath: 'bin/gsdd',
|
|
@@ -208,15 +223,15 @@ function buildPlanningCliHelperEntries() {
|
|
|
208
223
|
];
|
|
209
224
|
}
|
|
210
225
|
|
|
211
|
-
function buildPortableSkillEntries(workflows) {
|
|
226
|
+
function buildPortableSkillEntries(workflows, options = {}) {
|
|
212
227
|
return workflows.map((workflow) => ({
|
|
213
228
|
relativePath: `.agents/skills/${workflow.name}/SKILL.md`,
|
|
214
|
-
content: renderSkillContent(workflow),
|
|
229
|
+
content: renderSkillContent(workflow, options),
|
|
215
230
|
}));
|
|
216
231
|
}
|
|
217
232
|
|
|
218
|
-
function renderOpenCodeCommandContent(workflow) {
|
|
219
|
-
const workflowContent = getWorkflowContent(workflow.workflow);
|
|
233
|
+
function renderOpenCodeCommandContent(workflow, options = {}) {
|
|
234
|
+
const workflowContent = localizeStateDirReferences(getWorkflowContent(workflow.workflow), options);
|
|
220
235
|
return `---
|
|
221
236
|
description: ${workflow.description}
|
|
222
237
|
---
|
|
@@ -224,19 +239,23 @@ description: ${workflow.description}
|
|
|
224
239
|
${workflowContent}`;
|
|
225
240
|
}
|
|
226
241
|
|
|
227
|
-
function renderAgentsBoundedBlock() {
|
|
242
|
+
function renderAgentsBoundedBlock(options = {}) {
|
|
228
243
|
const blockPath = join(DISTILLED_DIR, 'templates', 'agents.block.md');
|
|
229
|
-
if (existsSync(blockPath)) return readFileSync(blockPath, 'utf-8').trim();
|
|
230
|
-
|
|
244
|
+
if (existsSync(blockPath)) return localizeStateDirReferences(readFileSync(blockPath, 'utf-8'), options).trim();
|
|
245
|
+
const stateDirName = normalizeStateDirName(options.stateDirName);
|
|
246
|
+
const planningLine = stateDirName === DEFAULT_STATE_DIR_NAME
|
|
247
|
+
? 'Planning state: `.work/` (legacy `.planning/` workspaces are still read).'
|
|
248
|
+
: 'Planning state: `.planning/` (legacy workspace; new Workspine projects use `.work/`).';
|
|
249
|
+
return `## GSDD Governance (Generated)\n\n- Framework: GSDD\n- ${planningLine}\n- Workflows: .agents/skills/gsdd-*/SKILL.md`;
|
|
231
250
|
}
|
|
232
251
|
|
|
233
|
-
function renderAgentsFileContent() {
|
|
252
|
+
function renderAgentsFileContent(options = {}) {
|
|
234
253
|
const templatePath = join(DISTILLED_DIR, 'templates', 'agents.md');
|
|
235
254
|
if (existsSync(templatePath)) {
|
|
236
255
|
const template = readFileSync(templatePath, 'utf-8');
|
|
237
|
-
return template.replace('{{GSDD_BLOCK}}', renderAgentsBoundedBlock()).trimEnd() + '\n';
|
|
256
|
+
return template.replace('{{GSDD_BLOCK}}', renderAgentsBoundedBlock(options)).trimEnd() + '\n';
|
|
238
257
|
}
|
|
239
|
-
const block = renderAgentsBoundedBlock();
|
|
258
|
+
const block = renderAgentsBoundedBlock(options);
|
|
240
259
|
return `# AGENTS.md - GSDD Governance\n\n<!-- BEGIN GSDD -->\n${block}\n<!-- END GSDD -->\n`;
|
|
241
260
|
}
|
|
242
261
|
|
|
@@ -274,6 +293,7 @@ export {
|
|
|
274
293
|
buildPortableSkillEntries,
|
|
275
294
|
getDelegateContent,
|
|
276
295
|
getWorkflowContent,
|
|
296
|
+
localizeStateDirReferences,
|
|
277
297
|
renderAgentsBoundedBlock,
|
|
278
298
|
renderAgentsFileContent,
|
|
279
299
|
renderOpenCodeCommandContent,
|
|
@@ -28,7 +28,8 @@ import {
|
|
|
28
28
|
getRuntimeModelOverride,
|
|
29
29
|
loadProjectModelConfig,
|
|
30
30
|
resolveRuntimeAgentModel,
|
|
31
|
-
} from './
|
|
31
|
+
} from './config.mjs';
|
|
32
|
+
import { resolveStateDir } from './state-dir.mjs';
|
|
32
33
|
|
|
33
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
35
|
const __dirname = dirname(__filename);
|
|
@@ -68,7 +69,7 @@ function compareGeneratedFile({ cwd, runtime, relativePath, expectedContent, rep
|
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
function buildClaudeEntries({ cwd, workflows }) {
|
|
72
|
+
function buildClaudeEntries({ cwd, workflows, stateDirName = '.work' }) {
|
|
72
73
|
const checkerModelAlias = resolveRuntimeAgentModel({
|
|
73
74
|
cwd,
|
|
74
75
|
runtime: 'claude',
|
|
@@ -85,8 +86,8 @@ function buildClaudeEntries({ cwd, workflows }) {
|
|
|
85
86
|
const entries = workflows.map((workflow) => ({
|
|
86
87
|
relativePath: `.claude/skills/${workflow.name}/SKILL.md`,
|
|
87
88
|
expectedContent: workflow.name === 'gsdd-plan'
|
|
88
|
-
? renderClaudePlanSkill()
|
|
89
|
-
: renderSkillContent(workflow),
|
|
89
|
+
? renderClaudePlanSkill({ stateDirName })
|
|
90
|
+
: renderSkillContent(workflow, { stateDirName }),
|
|
90
91
|
}));
|
|
91
92
|
|
|
92
93
|
entries.push(
|
|
@@ -107,7 +108,7 @@ function buildClaudeEntries({ cwd, workflows }) {
|
|
|
107
108
|
return entries;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
function buildOpenCodeEntries({ cwd, workflows }) {
|
|
111
|
+
function buildOpenCodeEntries({ cwd, workflows, stateDirName = '.work' }) {
|
|
111
112
|
const config = loadProjectModelConfig(cwd);
|
|
112
113
|
const checkerModelId = getRuntimeModelOverride(config, 'opencode', 'plan-checker');
|
|
113
114
|
const explorerModelId = getRuntimeModelOverride(config, 'opencode', 'approach-explorer');
|
|
@@ -115,8 +116,8 @@ function buildOpenCodeEntries({ cwd, workflows }) {
|
|
|
115
116
|
const entries = workflows.map((workflow) => ({
|
|
116
117
|
relativePath: `.opencode/commands/${workflow.name}.md`,
|
|
117
118
|
expectedContent: workflow.name === 'gsdd-plan'
|
|
118
|
-
? renderOpenCodePlanCommand()
|
|
119
|
-
: renderOpenCodeCommandContent(workflow),
|
|
119
|
+
? renderOpenCodePlanCommand({ stateDirName })
|
|
120
|
+
: renderOpenCodeCommandContent(workflow, { stateDirName }),
|
|
120
121
|
}));
|
|
121
122
|
|
|
122
123
|
entries.push(
|
|
@@ -150,31 +151,33 @@ function buildCodexEntries({ cwd }) {
|
|
|
150
151
|
];
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
function buildWorkspaceHelperEntries() {
|
|
154
|
+
function buildWorkspaceHelperEntries(stateDirName) {
|
|
154
155
|
return buildPlanningCliHelperEntries({
|
|
155
156
|
packageName: PACKAGE_JSON.name,
|
|
156
157
|
packageVersion: PACKAGE_JSON.version,
|
|
158
|
+
stateDirName,
|
|
157
159
|
}).map((entry) => ({
|
|
158
|
-
relativePath:
|
|
160
|
+
relativePath: `${stateDirName}/${entry.relativePath}`,
|
|
159
161
|
expectedContent: entry.content,
|
|
160
162
|
}));
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
export function collectExpectedRuntimeSurfaceGroups({ cwd = process.cwd(), workflows }) {
|
|
166
|
+
const stateDirName = resolveStateDir(cwd).name;
|
|
164
167
|
return [
|
|
165
168
|
{
|
|
166
169
|
runtime: 'workspace-helper',
|
|
167
170
|
label: 'workspace workflow helper',
|
|
168
|
-
root:
|
|
171
|
+
root: `${stateDirName}/bin`,
|
|
169
172
|
repairCommand: 'npx -y gsdd-cli update',
|
|
170
|
-
entries: buildWorkspaceHelperEntries(),
|
|
173
|
+
entries: buildWorkspaceHelperEntries(stateDirName),
|
|
171
174
|
},
|
|
172
175
|
{
|
|
173
176
|
runtime: 'portable',
|
|
174
177
|
label: 'portable skills',
|
|
175
178
|
root: '.agents/skills',
|
|
176
179
|
repairCommand: 'npx -y gsdd-cli update',
|
|
177
|
-
entries: buildPortableSkillEntries(workflows).map((entry) => ({
|
|
180
|
+
entries: buildPortableSkillEntries(workflows, { stateDirName }).map((entry) => ({
|
|
178
181
|
relativePath: entry.relativePath,
|
|
179
182
|
expectedContent: entry.content,
|
|
180
183
|
})),
|
|
@@ -184,14 +187,14 @@ export function collectExpectedRuntimeSurfaceGroups({ cwd = process.cwd(), workf
|
|
|
184
187
|
label: 'Claude Code native surfaces',
|
|
185
188
|
root: '.claude',
|
|
186
189
|
repairCommand: 'npx -y gsdd-cli update --tools claude',
|
|
187
|
-
entries: buildClaudeEntries({ cwd, workflows }),
|
|
190
|
+
entries: buildClaudeEntries({ cwd, workflows, stateDirName }),
|
|
188
191
|
},
|
|
189
192
|
{
|
|
190
193
|
runtime: 'opencode',
|
|
191
194
|
label: 'OpenCode native surfaces',
|
|
192
195
|
root: '.opencode',
|
|
193
196
|
repairCommand: 'npx -y gsdd-cli update --tools opencode',
|
|
194
|
-
entries: buildOpenCodeEntries({ cwd, workflows }),
|
|
197
|
+
entries: buildOpenCodeEntries({ cwd, workflows, stateDirName }),
|
|
195
198
|
},
|
|
196
199
|
{
|
|
197
200
|
runtime: 'codex',
|
|
@@ -206,7 +209,7 @@ export function collectExpectedRuntimeSurfaceGroups({ cwd = process.cwd(), workf
|
|
|
206
209
|
export function evaluateRuntimeFreshness({ cwd = process.cwd(), workflows = [] }) {
|
|
207
210
|
const groups = collectExpectedRuntimeSurfaceGroups({ cwd, workflows }).map((group) => {
|
|
208
211
|
const installed = group.runtime === 'workspace-helper'
|
|
209
|
-
? existsSync(
|
|
212
|
+
? existsSync(resolveStateDir(cwd).dir)
|
|
210
213
|
: existsSync(join(cwd, group.root));
|
|
211
214
|
const comparisons = installed
|
|
212
215
|
? group.entries.map((entry) => compareGeneratedFile({
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// state-dir.mjs — Single source of truth for the Workspine state directory.
|
|
2
|
+
//
|
|
3
|
+
// Workspine keeps everything in ONE folder: .work/. Older projects used
|
|
4
|
+
// .planning/. This module is the only place that decides which folder the tool
|
|
5
|
+
// reads and writes for a given repo root. No other module may hardcode the name.
|
|
6
|
+
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
export const STATE_DIR_NAME = '.work';
|
|
11
|
+
export const LEGACY_STATE_DIR_NAME = '.planning';
|
|
12
|
+
|
|
13
|
+
export const MIGRATION_NOTICE =
|
|
14
|
+
'Note: reading legacy .planning/ — new Workspine projects use .work/. Your .planning/ still works; move its files under .work/ when convenient.';
|
|
15
|
+
|
|
16
|
+
// True when a repo root already holds Workspine state in EITHER the current
|
|
17
|
+
// (.work) or legacy (.planning) location. Used for workspace-root discovery.
|
|
18
|
+
export function hasStateMarker(root) {
|
|
19
|
+
return (
|
|
20
|
+
existsSync(join(root, STATE_DIR_NAME, 'config.json')) ||
|
|
21
|
+
existsSync(join(root, STATE_DIR_NAME)) ||
|
|
22
|
+
existsSync(join(root, LEGACY_STATE_DIR_NAME, 'config.json')) ||
|
|
23
|
+
existsSync(join(root, LEGACY_STATE_DIR_NAME))
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Decide which state directory to use for a repo root.
|
|
28
|
+
// Precedence (config.json is the "really initialized" marker):
|
|
29
|
+
// 1. .work/config.json -> .work (migration done / new project)
|
|
30
|
+
// 2. .planning/config.json -> .planning (legacy; dual-read + notice)
|
|
31
|
+
// 3. .work/ dir -> .work
|
|
32
|
+
// 4. .planning/ dir -> .planning (legacy; dual-read + notice)
|
|
33
|
+
// 5. neither -> .work (brand-new repo default)
|
|
34
|
+
export function resolveStateDir(root) {
|
|
35
|
+
const workDir = join(root, STATE_DIR_NAME);
|
|
36
|
+
const legacyDir = join(root, LEGACY_STATE_DIR_NAME);
|
|
37
|
+
const work = { dir: workDir, name: STATE_DIR_NAME, legacy: false, migrationNotice: null };
|
|
38
|
+
const legacy = { dir: legacyDir, name: LEGACY_STATE_DIR_NAME, legacy: true, migrationNotice: MIGRATION_NOTICE };
|
|
39
|
+
|
|
40
|
+
if (existsSync(join(workDir, 'config.json'))) return work;
|
|
41
|
+
if (existsSync(join(legacyDir, 'config.json'))) return legacy;
|
|
42
|
+
if (existsSync(workDir)) return work;
|
|
43
|
+
if (existsSync(legacyDir)) return legacy;
|
|
44
|
+
return work;
|
|
45
|
+
}
|
package/bin/lib/templates.mjs
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
// templates.mjs - Project template and role installation/refresh helpers
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, cpSync, unlinkSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
5
|
+
import { basename, join } from 'path';
|
|
5
6
|
import { fileHash, readManifest } from './manifest.mjs';
|
|
7
|
+
import { localizeStateDirReferences } from './rendering.mjs';
|
|
6
8
|
|
|
7
|
-
export function installProjectTemplates({ planningDir, distilledDir, agentsDir }) {
|
|
9
|
+
export function installProjectTemplates({ planningDir, distilledDir, agentsDir, stateDirName = '.work' }) {
|
|
8
10
|
const localTemplatesDir = join(planningDir, 'templates');
|
|
9
11
|
const globalTemplatesDir = join(distilledDir, 'templates');
|
|
12
|
+
const stateName = basename(planningDir);
|
|
10
13
|
|
|
11
14
|
if (!existsSync(localTemplatesDir)) {
|
|
12
15
|
if (existsSync(globalTemplatesDir)) {
|
|
13
|
-
|
|
14
|
-
console.log(
|
|
16
|
+
copyTemplateTree(globalTemplatesDir, localTemplatesDir, { stateDirName });
|
|
17
|
+
console.log(` - copied templates to ${stateName}/templates/`);
|
|
15
18
|
// Warn-only by design: init should not fail on missing templates because
|
|
16
19
|
// the user may still proceed and fix later. The hard gate lives in
|
|
17
20
|
// `gsdd health` (E6/E7/E8) which reports these as errors. This is the
|
|
@@ -33,7 +36,7 @@ export function installProjectTemplates({ planningDir, distilledDir, agentsDir }
|
|
|
33
36
|
console.log(' - WARN: missing distilled/templates/; cannot copy templates');
|
|
34
37
|
}
|
|
35
38
|
} else {
|
|
36
|
-
console.log(
|
|
39
|
+
console.log(` - ${stateName}/templates/ already exists`);
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
const localRolesDir = join(localTemplatesDir, 'roles');
|
|
@@ -41,18 +44,18 @@ export function installProjectTemplates({ planningDir, distilledDir, agentsDir }
|
|
|
41
44
|
if (existsSync(agentsDir)) {
|
|
42
45
|
mkdirSync(localRolesDir, { recursive: true });
|
|
43
46
|
for (const file of listRoleFiles(agentsDir)) {
|
|
44
|
-
|
|
47
|
+
copyTemplateFile(join(agentsDir, file), join(localRolesDir, file), { stateDirName });
|
|
45
48
|
}
|
|
46
|
-
console.log(
|
|
49
|
+
console.log(` - copied role contracts to ${stateName}/templates/roles/`);
|
|
47
50
|
} else {
|
|
48
51
|
console.log(' - WARN: missing agents/; cannot copy role contracts');
|
|
49
52
|
}
|
|
50
53
|
} else {
|
|
51
|
-
console.log(
|
|
54
|
+
console.log(` - ${stateName}/templates/roles/ already exists`);
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
export function refreshTemplates({ planningDir, distilledDir, agentsDir, isDry = false }) {
|
|
58
|
+
export function refreshTemplates({ planningDir, distilledDir, agentsDir, isDry = false, stateDirName = '.work' }) {
|
|
56
59
|
const existingManifest = readManifest(planningDir);
|
|
57
60
|
const globalTemplatesDir = join(distilledDir, 'templates');
|
|
58
61
|
const localTemplatesDir = join(planningDir, 'templates');
|
|
@@ -65,11 +68,45 @@ export function refreshTemplates({ planningDir, distilledDir, agentsDir, isDry =
|
|
|
65
68
|
];
|
|
66
69
|
|
|
67
70
|
for (const category of categories) {
|
|
68
|
-
refreshCategory(category, existingManifest, isDry);
|
|
71
|
+
refreshCategory(category, existingManifest, isDry, { stateDirName });
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingManifest, isDry);
|
|
72
|
-
refreshRoles(agentsDir, join(localTemplatesDir, 'roles'), existingManifest, isDry);
|
|
74
|
+
refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingManifest, isDry, { stateDirName });
|
|
75
|
+
refreshRoles(agentsDir, join(localTemplatesDir, 'roles'), existingManifest, isDry, { stateDirName });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function contentHash(content) {
|
|
79
|
+
return createHash('sha256').update(content).digest('hex');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function localizedTemplateContent(srcPath, { stateDirName = '.work' } = {}) {
|
|
83
|
+
return localizeStateDirReferences(readFileSync(srcPath, 'utf-8'), { stateDirName });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sourceTemplateHash(srcPath, { stateDirName = '.work' } = {}) {
|
|
87
|
+
if (!srcPath.endsWith('.md')) return fileHash(srcPath);
|
|
88
|
+
return contentHash(localizedTemplateContent(srcPath, { stateDirName }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function copyTemplateFile(srcPath, destPath, { stateDirName = '.work' } = {}) {
|
|
92
|
+
if (!srcPath.endsWith('.md')) {
|
|
93
|
+
cpSync(srcPath, destPath);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
writeFileSync(destPath, localizedTemplateContent(srcPath, { stateDirName }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function copyTemplateTree(srcDir, destDir, { stateDirName = '.work' } = {}) {
|
|
100
|
+
mkdirSync(destDir, { recursive: true });
|
|
101
|
+
for (const entry of readdirSync(srcDir)) {
|
|
102
|
+
const srcPath = join(srcDir, entry);
|
|
103
|
+
const destPath = join(destDir, entry);
|
|
104
|
+
if (statSync(srcPath).isDirectory()) {
|
|
105
|
+
copyTemplateTree(srcPath, destPath, { stateDirName });
|
|
106
|
+
} else {
|
|
107
|
+
copyTemplateFile(srcPath, destPath, { stateDirName });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
73
110
|
}
|
|
74
111
|
|
|
75
112
|
function listRoleFiles(agentsDir) {
|
|
@@ -78,7 +115,7 @@ function listRoleFiles(agentsDir) {
|
|
|
78
115
|
);
|
|
79
116
|
}
|
|
80
117
|
|
|
81
|
-
function refreshCategory({ name, src, dest, manifestKey }, existingManifest, isDry) {
|
|
118
|
+
function refreshCategory({ name, src, dest, manifestKey }, existingManifest, isDry, { stateDirName = '.work' } = {}) {
|
|
82
119
|
if (!existsSync(src)) return;
|
|
83
120
|
if (!existsSync(dest) && !isDry) {
|
|
84
121
|
mkdirSync(dest, { recursive: true });
|
|
@@ -91,7 +128,7 @@ function refreshCategory({ name, src, dest, manifestKey }, existingManifest, isD
|
|
|
91
128
|
for (const file of sourceFiles) {
|
|
92
129
|
const srcPath = join(src, file);
|
|
93
130
|
const destPath = join(dest, file);
|
|
94
|
-
const srcHash =
|
|
131
|
+
const srcHash = sourceTemplateHash(srcPath, { stateDirName });
|
|
95
132
|
|
|
96
133
|
if (existsSync(destPath)) {
|
|
97
134
|
const destHash = fileHash(destPath);
|
|
@@ -106,7 +143,7 @@ function refreshCategory({ name, src, dest, manifestKey }, existingManifest, isD
|
|
|
106
143
|
if (isDry) {
|
|
107
144
|
console.log(` - would refresh ${name}/${file}`);
|
|
108
145
|
} else {
|
|
109
|
-
|
|
146
|
+
copyTemplateFile(srcPath, destPath, { stateDirName });
|
|
110
147
|
console.log(` - refreshed ${name}/${file}`);
|
|
111
148
|
}
|
|
112
149
|
}
|
|
@@ -126,7 +163,7 @@ function refreshCategory({ name, src, dest, manifestKey }, existingManifest, isD
|
|
|
126
163
|
}
|
|
127
164
|
}
|
|
128
165
|
|
|
129
|
-
function refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingManifest, isDry) {
|
|
166
|
+
function refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingManifest, isDry, { stateDirName = '.work' } = {}) {
|
|
130
167
|
if (!existsSync(globalTemplatesDir)) return;
|
|
131
168
|
|
|
132
169
|
const manifestHashes = existingManifest?.templates?.root || null;
|
|
@@ -135,7 +172,7 @@ function refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingMan
|
|
|
135
172
|
for (const file of sourceFiles) {
|
|
136
173
|
const srcPath = join(globalTemplatesDir, file);
|
|
137
174
|
const destPath = join(localTemplatesDir, file);
|
|
138
|
-
const srcHash =
|
|
175
|
+
const srcHash = sourceTemplateHash(srcPath, { stateDirName });
|
|
139
176
|
|
|
140
177
|
if (existsSync(destPath)) {
|
|
141
178
|
const destHash = fileHash(destPath);
|
|
@@ -150,7 +187,7 @@ function refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingMan
|
|
|
150
187
|
if (isDry) {
|
|
151
188
|
console.log(` - would refresh templates/${file}`);
|
|
152
189
|
} else {
|
|
153
|
-
|
|
190
|
+
copyTemplateFile(srcPath, destPath, { stateDirName });
|
|
154
191
|
console.log(` - refreshed templates/${file}`);
|
|
155
192
|
}
|
|
156
193
|
}
|
|
@@ -174,7 +211,7 @@ function refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingMan
|
|
|
174
211
|
}
|
|
175
212
|
}
|
|
176
213
|
|
|
177
|
-
function refreshRoles(agentsDir, localRolesDir, existingManifest, isDry) {
|
|
214
|
+
function refreshRoles(agentsDir, localRolesDir, existingManifest, isDry, { stateDirName = '.work' } = {}) {
|
|
178
215
|
if (!existsSync(agentsDir)) return;
|
|
179
216
|
if (!existsSync(localRolesDir) && !isDry) {
|
|
180
217
|
mkdirSync(localRolesDir, { recursive: true });
|
|
@@ -189,7 +226,7 @@ function refreshRoles(agentsDir, localRolesDir, existingManifest, isDry) {
|
|
|
189
226
|
for (const file of sourceFiles) {
|
|
190
227
|
const srcPath = join(agentsDir, file);
|
|
191
228
|
const destPath = join(localRolesDir, file);
|
|
192
|
-
const srcHash =
|
|
229
|
+
const srcHash = sourceTemplateHash(srcPath, { stateDirName });
|
|
193
230
|
|
|
194
231
|
if (existsSync(destPath)) {
|
|
195
232
|
const destHash = fileHash(destPath);
|
|
@@ -204,7 +241,7 @@ function refreshRoles(agentsDir, localRolesDir, existingManifest, isDry) {
|
|
|
204
241
|
if (isDry) {
|
|
205
242
|
console.log(` - would refresh roles/${file}`);
|
|
206
243
|
} else {
|
|
207
|
-
|
|
244
|
+
copyTemplateFile(srcPath, destPath, { stateDirName });
|
|
208
245
|
console.log(` - refreshed roles/${file}`);
|
|
209
246
|
}
|
|
210
247
|
}
|
package/bin/lib/work-context.mjs
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
writeSync,
|
|
13
13
|
} from 'fs';
|
|
14
14
|
import { basename, dirname, join, relative, resolve } from 'path';
|
|
15
|
+
import { evaluateLifecycleState } from './lifecycle-state.mjs';
|
|
16
|
+
import { resolveStateDir } from './state-dir.mjs';
|
|
15
17
|
|
|
16
18
|
export const WORK_DIR_NAME = '.work';
|
|
17
19
|
|
|
@@ -657,14 +659,22 @@ export function inspectWorkContext(cwd = process.cwd()) {
|
|
|
657
659
|
const questions = readOpenQuestions(paths.workDir);
|
|
658
660
|
const evidence = readJsonIfExists(paths.evidenceManifest);
|
|
659
661
|
const graph = readGraphEvents(paths.workDir);
|
|
660
|
-
const planningDir =
|
|
662
|
+
const { dir: planningDir, name: stateDirName, migrationNotice } = resolveStateDir(paths.root);
|
|
663
|
+
const lifecycle = evaluateLifecycleState({ planningDir });
|
|
661
664
|
const planning = {
|
|
662
665
|
exists: existsSync(planningDir),
|
|
663
666
|
has_spec: existsSync(join(planningDir, 'SPEC.md')),
|
|
664
667
|
has_roadmap: existsSync(join(planningDir, 'ROADMAP.md')),
|
|
665
668
|
has_milestones: existsSync(join(planningDir, 'MILESTONES.md')),
|
|
666
669
|
has_config: existsSync(join(planningDir, 'config.json')),
|
|
670
|
+
has_brownfield_change: lifecycle.brownfieldChange.exists,
|
|
671
|
+
non_phase_state: lifecycle.nonPhaseState,
|
|
672
|
+
brownfield_change: lifecycle.brownfieldChange,
|
|
673
|
+
current_phase: lifecycle.currentPhase?.number || null,
|
|
674
|
+
next_phase: lifecycle.nextPhase?.number || null,
|
|
675
|
+
counts: lifecycle.counts,
|
|
667
676
|
phases: scanPhaseEvidence(planningDir),
|
|
677
|
+
state_dir_name: stateDirName,
|
|
668
678
|
};
|
|
669
679
|
return {
|
|
670
680
|
paths,
|
|
@@ -676,6 +686,7 @@ export function inspectWorkContext(cwd = process.cwd()) {
|
|
|
676
686
|
evidence,
|
|
677
687
|
graph,
|
|
678
688
|
planning,
|
|
689
|
+
migration_notice: migrationNotice,
|
|
679
690
|
milestone: inspectWorkMilestone(paths.workDir),
|
|
680
691
|
focus_exists: existsSync(paths.focus),
|
|
681
692
|
handoff_exists: existsSync(paths.handoff),
|
package/bin/lib/workflows.mjs
CHANGED
|
@@ -17,7 +17,6 @@ export const WORKFLOWS = [
|
|
|
17
17
|
defineWorkflow({ name: 'gsdd-audit-milestone', workflow: 'audit-milestone.md', description: 'Audit a completed milestone - cross-phase integration, requirements coverage, E2E flows' }),
|
|
18
18
|
defineWorkflow({ name: 'gsdd-complete-milestone', workflow: 'complete-milestone.md', description: 'Complete milestone - archive, evolve spec, collapse roadmap' }),
|
|
19
19
|
defineWorkflow({ name: 'gsdd-new-milestone', workflow: 'new-milestone.md', description: 'New milestone - gather goals, define requirements, create roadmap phases' }),
|
|
20
|
-
defineWorkflow({ name: 'gsdd-plan-milestone-gaps', workflow: 'plan-milestone-gaps.md', description: 'Plan gap closure phases from audit results' }),
|
|
21
20
|
defineWorkflow({ name: 'gsdd-quick', workflow: 'quick.md', description: 'Quick task - plan and execute a sub-hour task outside the phase cycle' }),
|
|
22
21
|
defineWorkflow({ name: 'gsdd-pause', workflow: 'pause.md', description: 'Pause work - save session context for seamless resumption' }),
|
|
23
22
|
defineWorkflow({ name: 'gsdd-resume', workflow: 'resume.md', description: 'Resume work - restore context and route to next action' }),
|