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.
Files changed (84) hide show
  1. package/README.md +53 -22
  2. package/agents/DISTILLATION.md +2 -2
  3. package/agents/README.md +4 -4
  4. package/agents/approach-explorer.md +4 -4
  5. package/agents/executor.md +20 -20
  6. package/agents/integration-checker.md +2 -2
  7. package/agents/planner.md +10 -26
  8. package/agents/researcher.md +2 -2
  9. package/agents/roadmapper.md +6 -6
  10. package/agents/synthesizer.md +18 -18
  11. package/agents/verifier.md +3 -3
  12. package/bin/adapters/agents.mjs +3 -3
  13. package/bin/adapters/claude.mjs +16 -14
  14. package/bin/adapters/opencode.mjs +15 -12
  15. package/bin/gsdd.mjs +16 -13
  16. package/bin/lib/{models.mjs → config.mjs} +23 -16
  17. package/bin/lib/control-map.mjs +17 -488
  18. package/bin/lib/health-truth.mjs +8 -13
  19. package/bin/lib/health.mjs +25 -39
  20. package/bin/lib/init-flow.mjs +44 -38
  21. package/bin/lib/init-prompts.mjs +3 -3
  22. package/bin/lib/init-runtime.mjs +11 -30
  23. package/bin/lib/lifecycle-preflight.mjs +97 -410
  24. package/bin/lib/lifecycle-state.mjs +2 -1
  25. package/bin/lib/next.mjs +243 -20
  26. package/bin/lib/phase.mjs +706 -280
  27. package/bin/lib/plan-constants.mjs +0 -5
  28. package/bin/lib/rendering.mjs +64 -44
  29. package/bin/lib/runtime-freshness.mjs +18 -15
  30. package/bin/lib/state-dir.mjs +45 -0
  31. package/bin/lib/templates.mjs +59 -22
  32. package/bin/lib/work-context.mjs +12 -1
  33. package/bin/lib/workflows.mjs +0 -1
  34. package/bin/lib/workspace-root.mjs +11 -6
  35. package/distilled/DESIGN.md +89 -31
  36. package/distilled/EVIDENCE-INDEX.md +18 -5
  37. package/distilled/README.md +23 -33
  38. package/distilled/SKILL.md +9 -10
  39. package/distilled/templates/agents.block.md +5 -5
  40. package/distilled/templates/approach.md +3 -3
  41. package/distilled/templates/auth-matrix.md +2 -2
  42. package/distilled/templates/brownfield-change/CHANGE.md +1 -1
  43. package/distilled/templates/delegates/approach-explorer.md +5 -5
  44. package/distilled/templates/delegates/mapper-arch.md +3 -3
  45. package/distilled/templates/delegates/mapper-concerns.md +4 -4
  46. package/distilled/templates/delegates/mapper-quality.md +3 -3
  47. package/distilled/templates/delegates/mapper-tech.md +3 -3
  48. package/distilled/templates/delegates/plan-checker.md +18 -19
  49. package/distilled/templates/delegates/researcher-architecture.md +3 -3
  50. package/distilled/templates/delegates/researcher-features.md +3 -3
  51. package/distilled/templates/delegates/researcher-pitfalls.md +3 -3
  52. package/distilled/templates/delegates/researcher-stack.md +3 -3
  53. package/distilled/templates/delegates/researcher-synthesizer.md +7 -7
  54. package/distilled/templates/research/architecture.md +1 -1
  55. package/distilled/templates/research/pitfalls.md +1 -1
  56. package/distilled/templates/research/stack.md +1 -1
  57. package/distilled/templates/roadmap.md +4 -4
  58. package/distilled/templates/spec.md +2 -2
  59. package/distilled/templates/ui-proof.md +81 -181
  60. package/distilled/workflows/audit-milestone.md +23 -23
  61. package/distilled/workflows/complete-milestone.md +35 -35
  62. package/distilled/workflows/execute.md +34 -35
  63. package/distilled/workflows/map-codebase.md +30 -30
  64. package/distilled/workflows/new-milestone.md +18 -18
  65. package/distilled/workflows/new-project.md +45 -45
  66. package/distilled/workflows/pause.md +15 -15
  67. package/distilled/workflows/plan.md +106 -114
  68. package/distilled/workflows/progress.md +40 -39
  69. package/distilled/workflows/quick.md +49 -50
  70. package/distilled/workflows/resume.md +23 -22
  71. package/distilled/workflows/verify-work.md +7 -7
  72. package/distilled/workflows/verify.md +26 -26
  73. package/docs/BROWNFIELD-PROOF.md +1 -1
  74. package/docs/RUNTIME-SUPPORT.md +13 -13
  75. package/docs/USER-GUIDE.md +26 -21
  76. package/docs/claude/context-monitor.md +1 -1
  77. package/docs/proof/consumer-node-cli/README.md +1 -1
  78. package/package.json +3 -3
  79. package/bin/lib/closeout-report.mjs +0 -318
  80. package/bin/lib/evidence-contract.mjs +0 -325
  81. package/bin/lib/provenance.mjs +0 -390
  82. package/bin/lib/session-fingerprint.mjs +0 -223
  83. package/bin/lib/ui-proof.mjs +0 -1007
  84. 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
 
@@ -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
- 'session-fingerprint.mjs',
18
- 'ui-proof.mjs',
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 { cmdSessionFingerprint } from './lib/session-fingerprint.mjs';
53
- import { cmdUiProof } from './lib/ui-proof.mjs';
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 cmdCloseoutReport = createCmdCloseoutReport(HELPER_CONTEXT);
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
- 'session-fingerprint': cmdSessionFingerprint,
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 .planning/bin/gsdd.mjs [--workspace-root <path>] <command> [args]',
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 .planning/bin/gsdd.mjs file-op delete .planning/.continue-here.bak --missing ok',
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 .planning/bin/gsdd.mjs phase-status 1 done',
85
- ' verify <N> Run direct phase artifact and UI-proof gate checks',
86
- ' Example: node .planning/bin/gsdd.mjs verify 1',
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 .planning/bin/gsdd.mjs lifecycle-preflight verify 1 --expects-mutation phase-status',
90
- ' session-fingerprint write [--allow-changed <ROADMAP.md,SPEC.md,config.json>]',
91
- ' Rebaseline planning-state drift after reviewing changed planning files',
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
- return '## GSDD Governance (Generated)\n\n- Framework: GSDD\n- Planning: .planning/\n- Workflows: .agents/skills/gsdd-*/SKILL.md';
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 './models.mjs';
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: `.planning/${entry.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: '.planning/bin',
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(join(cwd, '.planning'))
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
+ }
@@ -1,17 +1,20 @@
1
1
  // templates.mjs - Project template and role installation/refresh helpers
2
2
 
3
- import { existsSync, mkdirSync, readdirSync, cpSync, unlinkSync } from 'fs';
4
- import { join } from 'path';
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
- cpSync(globalTemplatesDir, localTemplatesDir, { recursive: true });
14
- console.log(' - copied templates to .planning/templates/');
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(' - .planning/templates/ already exists');
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
- cpSync(join(agentsDir, file), join(localRolesDir, file));
47
+ copyTemplateFile(join(agentsDir, file), join(localRolesDir, file), { stateDirName });
45
48
  }
46
- console.log(' - copied role contracts to .planning/templates/roles/');
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(' - .planning/templates/roles/ already exists');
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 = fileHash(srcPath);
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
- cpSync(srcPath, destPath);
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 = fileHash(srcPath);
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
- cpSync(srcPath, destPath);
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 = fileHash(srcPath);
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
- cpSync(srcPath, destPath);
244
+ copyTemplateFile(srcPath, destPath, { stateDirName });
208
245
  console.log(` - refreshed roles/${file}`);
209
246
  }
210
247
  }
@@ -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 = join(paths.root, '.planning');
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),
@@ -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' }),