@@ -10,6 +10,7 @@ import { output } from './cli-utils.mjs';
1010import { runTruthChecks , TRUTH_CHECK_IDS } from './health-truth.mjs' ;
1111import { evaluateLifecycleState } from './lifecycle-state.mjs' ;
1212import { evaluateRuntimeFreshness } from './runtime-freshness.mjs' ;
13+ import { resolveWorkspaceContext } from './workspace-root.mjs' ;
1314
1415/**
1516 * Factory function returning the health command.
@@ -18,17 +19,26 @@ import { evaluateRuntimeFreshness } from './runtime-freshness.mjs';
1819export function createCmdHealth ( ctx ) {
1920 return async function cmdHealth ( ...healthArgs ) {
2021 const jsonMode = healthArgs . includes ( '--json' ) ;
21- const cwd = process . cwd ( ) ;
22- const planningDir = join ( cwd , '.planning' ) ;
22+ const { planningDir, workspaceRoot, invalid, error } = resolveWorkspaceContext ( healthArgs ) ;
23+ if ( invalid ) {
24+ if ( jsonMode ) {
25+ output ( { status : 'broken' , errors : [ { id : 'E1' , severity : 'ERROR' , message : error , fix : 'Pass --workspace-root with a real path or remove the flag.' } ] , warnings : [ ] , info : [ ] } ) ;
26+ } else {
27+ console . log ( error ) ;
28+ }
29+ process . exitCode = 1 ;
30+ return ;
31+ }
32+ const cwd = workspaceRoot ;
2333 const frameworkSourceMode = isFrameworkSourceRepo ( cwd ) ;
2434 const healthCheckIds = [ 'E1' , 'E2' , 'E3' , 'E4' , 'E5' , 'E6' , 'E7' , 'E8' , 'W1' , 'W2' , 'W3' , 'W4' , 'W5' , 'W6' , ...TRUTH_CHECK_IDS , 'I1' , 'I2' , 'I3' ] ;
2535
2636 // Pre-init guard
2737 if ( ! existsSync ( join ( planningDir , 'config.json' ) ) ) {
2838 if ( jsonMode ) {
29- output ( { status : 'broken' , errors : [ { id : 'E1' , severity : 'ERROR' , message : '.planning/config.json missing' , fix : 'Run `gsdd init`' } ] , warnings : [ ] , info : [ ] } ) ;
39+ output ( { status : 'broken' , errors : [ { id : 'E1' , severity : 'ERROR' , message : '.planning/config.json missing' , fix : 'Run `npx -y gsdd-cli init`' } ] , warnings : [ ] , info : [ ] } ) ;
3040 } else {
31- console . log ( 'Not initialized. Run `gsdd init`.' ) ;
41+ console . log ( 'Not initialized. Run `npx -y gsdd-cli init`. If `gsdd` is installed globally, `gsdd init` is also fine .' ) ;
3242 }
3343 process . exitCode = 1 ;
3444 return ;
@@ -50,71 +60,73 @@ export function createCmdHealth(ctx) {
5060 const requiredFields = [ 'researchDepth' , 'modelProfile' , 'initVersion' ] ;
5161 const missing = requiredFields . filter ( ( f ) => ! ( f in config ) ) ;
5262 if ( missing . length > 0 ) {
53- errors . push ( { id : 'E2' , severity : 'ERROR' , message : `config.json missing required fields: ${ missing . join ( ', ' ) } ` , fix : 'Run `gsdd init` to regenerate' } ) ;
63+ errors . push ( { id : 'E2' , severity : 'ERROR' , message : `config.json missing required fields: ${ missing . join ( ', ' ) } ` , fix : 'Run `npx -y gsdd-cli init` to regenerate' } ) ;
5464 }
5565 } catch {
56- errors . push ( { id : 'E1' , severity : 'ERROR' , message : '.planning/config.json is unparseable' , fix : 'Run `gsdd init`' } ) ;
66+ errors . push ( { id : 'E1' , severity : 'ERROR' , message : '.planning/config.json is unparseable' , fix : 'Run `npx -y gsdd-cli init`' } ) ;
5767 }
5868
5969 // E3: templates/ missing
6070 const templatesDir = join ( planningDir , 'templates' ) ;
71+ const runtimeHelpersDir = join ( planningDir , 'bin' ) ;
6172 const hasTemplatesDir = existsSync ( templatesDir ) ;
73+ const hasRuntimeHelpersDir = existsSync ( runtimeHelpersDir ) ;
6274 const rolesDir = join ( templatesDir , 'roles' ) ;
6375 const delegatesDir = join ( templatesDir , 'delegates' ) ;
6476 const hasRolesDir = hasTemplatesDir && existsSync ( rolesDir ) ;
6577 const hasDelegatesDir = hasTemplatesDir && existsSync ( delegatesDir ) ;
6678 const skipInstalledTemplateChecks = ! hasTemplatesDir && frameworkSourceMode ;
6779
6880 if ( ! hasTemplatesDir && ! skipInstalledTemplateChecks ) {
69- errors . push ( { id : 'E3' , severity : 'ERROR' , message : '.planning/templates/ missing' , fix : 'Run `gsdd update --templates`' } ) ;
81+ errors . push ( { id : 'E3' , severity : 'ERROR' , message : '.planning/templates/ missing' , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
7082 } else if ( hasTemplatesDir ) {
7183 // E4: roles/ missing or empty
7284 if ( ! hasRolesDir ) {
73- errors . push ( { id : 'E4' , severity : 'ERROR' , message : '.planning/templates/roles/ missing' , fix : 'Run `gsdd update --templates`' } ) ;
85+ errors . push ( { id : 'E4' , severity : 'ERROR' , message : '.planning/templates/roles/ missing' , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
7486 } else {
7587 const roleFiles = readdirSync ( rolesDir ) . filter ( ( f ) => f . endsWith ( '.md' ) ) ;
7688 if ( roleFiles . length === 0 ) {
77- errors . push ( { id : 'E4' , severity : 'ERROR' , message : '.planning/templates/roles/ has 0 role files' , fix : 'Run `gsdd update --templates`' } ) ;
89+ errors . push ( { id : 'E4' , severity : 'ERROR' , message : '.planning/templates/roles/ has 0 role files' , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
7890 }
7991 }
8092
8193 // E5: delegates/ missing or empty
8294 if ( ! hasDelegatesDir ) {
83- errors . push ( { id : 'E5' , severity : 'ERROR' , message : '.planning/templates/delegates/ missing' , fix : 'Run `gsdd update --templates`' } ) ;
95+ errors . push ( { id : 'E5' , severity : 'ERROR' , message : '.planning/templates/delegates/ missing' , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
8496 } else {
8597 const delegateFiles = readdirSync ( delegatesDir ) . filter ( ( f ) => f . endsWith ( '.md' ) ) ;
8698 if ( delegateFiles . length === 0 ) {
87- errors . push ( { id : 'E5' , severity : 'ERROR' , message : '.planning/templates/delegates/ has 0 delegate files' , fix : 'Run `gsdd update --templates`' } ) ;
99+ errors . push ( { id : 'E5' , severity : 'ERROR' , message : '.planning/templates/delegates/ has 0 delegate files' , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
88100 }
89101 }
90102
91103 // E6: research/ missing or empty
92104 const researchDir = join ( templatesDir , 'research' ) ;
93105 if ( ! existsSync ( researchDir ) ) {
94- errors . push ( { id : 'E6' , severity : 'ERROR' , message : '.planning/templates/research/ missing' , fix : 'Run `gsdd update --templates`' } ) ;
106+ errors . push ( { id : 'E6' , severity : 'ERROR' , message : '.planning/templates/research/ missing' , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
95107 } else {
96108 const researchFiles = readdirSync ( researchDir ) . filter ( ( f ) => f . endsWith ( '.md' ) ) ;
97109 if ( researchFiles . length === 0 ) {
98- errors . push ( { id : 'E6' , severity : 'ERROR' , message : '.planning/templates/research/ has 0 template files' , fix : 'Run `gsdd update --templates`' } ) ;
110+ errors . push ( { id : 'E6' , severity : 'ERROR' , message : '.planning/templates/research/ has 0 template files' , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
99111 }
100112 }
101113
102114 // E7: codebase/ missing or empty
103115 const codebaseDir = join ( templatesDir , 'codebase' ) ;
104116 if ( ! existsSync ( codebaseDir ) ) {
105- errors . push ( { id : 'E7' , severity : 'ERROR' , message : '.planning/templates/codebase/ missing' , fix : 'Run `gsdd update --templates`' } ) ;
117+ errors . push ( { id : 'E7' , severity : 'ERROR' , message : '.planning/templates/codebase/ missing' , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
106118 } else {
107119 const codebaseFiles = readdirSync ( codebaseDir ) . filter ( ( f ) => f . endsWith ( '.md' ) ) ;
108120 if ( codebaseFiles . length === 0 ) {
109- errors . push ( { id : 'E7' , severity : 'ERROR' , message : '.planning/templates/codebase/ has 0 template files' , fix : 'Run `gsdd update --templates`' } ) ;
121+ errors . push ( { id : 'E7' , severity : 'ERROR' , message : '.planning/templates/codebase/ has 0 template files' , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
110122 }
111123 }
112124
113125 // E8: critical root template files missing
114126 const requiredRootFiles = [ 'spec.md' , 'roadmap.md' , 'auth-matrix.md' ] ;
115127 const missingRoot = requiredRootFiles . filter ( ( f ) => ! existsSync ( join ( templatesDir , f ) ) ) ;
116128 if ( missingRoot . length > 0 ) {
117- errors . push ( { id : 'E8' , severity : 'ERROR' , message : `.planning/templates/ missing critical root files: ${ missingRoot . join ( ', ' ) } ` , fix : 'Run `gsdd update --templates`' } ) ;
129+ errors . push ( { id : 'E8' , severity : 'ERROR' , message : `.planning/templates/ missing critical root files: ${ missingRoot . join ( ', ' ) } ` , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
118130 }
119131 }
120132
@@ -123,27 +135,28 @@ export function createCmdHealth(ctx) {
123135 // W1: generation-manifest.json missing
124136 const manifest = skipInstalledTemplateChecks ? null : readManifest ( planningDir ) ;
125137 if ( ! manifest && ! skipInstalledTemplateChecks ) {
126- warnings . push ( { id : 'W1' , severity : 'WARN' , message : 'generation-manifest.json missing' , fix : 'Run `gsdd update --templates ` to create' } ) ;
138+ warnings . push ( { id : 'W1' , severity : 'WARN' , message : 'generation-manifest.json missing' , fix : 'Run `npx -y gsdd-cli update ` to create' } ) ;
127139 }
128140
129141 // W2 + W3: template/role hash mismatches and missing files
130142 if ( manifest && hasTemplatesDir ) {
131143 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 } ,
144+ { name : 'delegates' , dir : delegatesDir , hashes : hasDelegatesDir ? manifest . templates ?. delegates : null , fixCommand : 'npx -y gsdd-cli update --templates' } ,
145+ { name : 'research' , dir : join ( templatesDir , 'research' ) , hashes : manifest . templates ?. research , fixCommand : 'npx -y gsdd-cli update --templates' } ,
146+ { name : 'codebase' , dir : join ( templatesDir , 'codebase' ) , hashes : manifest . templates ?. codebase , fixCommand : 'npx -y gsdd-cli update --templates' } ,
147+ { name : 'root templates' , dir : templatesDir , hashes : manifest . templates ?. root , fixCommand : 'npx -y gsdd-cli update --templates' } ,
148+ { name : 'roles' , dir : rolesDir , hashes : hasRolesDir ? manifest . roles : null , fixCommand : 'npx -y gsdd-cli update --templates' } ,
149+ { name : 'runtime helpers' , dir : planningDir , hashes : hasRuntimeHelpersDir ? manifest . runtimeHelpers : null , fixCommand : 'npx -y gsdd-cli update' } ,
137150 ] ;
138151
139152 for ( const cat of allCategories ) {
140153 if ( ! cat . hashes ) continue ;
141154 const result = detectModifications ( cat . dir , cat . hashes ) ;
142155 if ( result . modified . length > 0 ) {
143- warnings . push ( { id : 'W2' , severity : 'WARN' , message : `${ cat . name } : ${ result . modified . length } file(s) modified locally (${ result . modified . join ( ', ' ) } )` , fix : ' Intentional? Run `gsdd update --templates ` to reset' } ) ;
156+ 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` } ) ;
144157 }
145158 if ( result . missing . length > 0 ) {
146- warnings . push ( { id : 'W3' , severity : 'WARN' , message : `${ cat . name } : ${ result . missing . length } file(s) missing from disk (${ result . missing . join ( ', ' ) } )` , fix : ' Run `gsdd update --templates ` to restore' } ) ;
159+ 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` } ) ;
147160 }
148161 }
149162 }
@@ -186,20 +199,30 @@ export function createCmdHealth(ctx) {
186199 ] ;
187200 const hasAnyAdapter = adapterPaths . some ( ( p ) => existsSync ( p ) ) ;
188201 if ( ! hasAnyAdapter ) {
189- warnings . push ( { id : 'W6' , severity : 'WARN' , message : 'No adapter surfaces detected' , fix : 'Run `gsdd init --tools <platform>`' } ) ;
202+ warnings . push ( { id : 'W6' , severity : 'WARN' , message : 'No adapter surfaces detected' , fix : 'Run `npx -y gsdd-cli init --tools <platform>`' } ) ;
190203 }
191204
192205 const runtimeFreshnessReport = configOk && Array . isArray ( ctx . workflows )
193206 ? evaluateRuntimeFreshness ( { cwd, workflows : ctx . workflows } )
194207 : null ;
195208
196- warnings . push ( ...runTruthChecks ( planningDir , cwd , healthCheckIds , { runtimeFreshnessReport } ) ) ;
209+ warnings . push ( ...runTruthChecks ( planningDir , cwd , healthCheckIds , { runtimeFreshnessReport } ) . map ( ( warning ) => {
210+ if ( warning . id !== 'W10' ) return warning ;
211+ return {
212+ ...warning ,
213+ message : warning . message . replace (
214+ / ^ R O A D M A P \/ S P E C r e q u i r e m e n t s t a t u s d r i f t / ,
215+ 'ROADMAP lifecycle status drift (requirement checkbox and/or overview/detail phase status mismatch)'
216+ ) ,
217+ fix : 'Reconcile .planning/ROADMAP.md overview/detail phase markers and .planning/SPEC.md requirement checkboxes' ,
218+ } ;
219+ } ) ) ;
197220
198221 // --- INFO checks ---
199222
200223 // I1: generation manifest was produced by a different framework version
201224 if ( manifest && manifest . frameworkVersion && manifest . frameworkVersion !== ctx . frameworkVersion ) {
202- info . push ( { id : 'I1' , severity : 'INFO' , message : `Generation manifest frameworkVersion (${ manifest . frameworkVersion } ) differs from current framework version (${ ctx . frameworkVersion } )` , fix : 'Run `gsdd update --templates`' } ) ;
225+ info . push ( { id : 'I1' , severity : 'INFO' , message : `Generation manifest frameworkVersion (${ manifest . frameworkVersion } ) differs from current framework version (${ ctx . frameworkVersion } )` , fix : 'Run `npx -y gsdd-cli update --templates`' } ) ;
203226 }
204227
205228 // I2: Phase completion count
@@ -250,4 +273,3 @@ export function createCmdHealth(ctx) {
250273function isFrameworkSourceRepo ( cwd ) {
251274 return existsSync ( join ( cwd , 'distilled' , 'templates' ) ) && existsSync ( join ( cwd , 'distilled' , 'workflows' ) ) ;
252275}
253-
0 commit comments