gsdd-cli 0.16.1 → 0.18.1

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 (50) hide show
  1. package/README.md +111 -61
  2. package/agents/README.md +1 -1
  3. package/bin/adapters/claude.mjs +8 -1
  4. package/bin/adapters/codex.mjs +5 -1
  5. package/bin/adapters/opencode.mjs +7 -1
  6. package/bin/gsdd.mjs +25 -20
  7. package/bin/lib/evidence-contract.mjs +112 -0
  8. package/bin/lib/health-truth.mjs +29 -31
  9. package/bin/lib/health.mjs +61 -106
  10. package/bin/lib/init-flow.mjs +91 -34
  11. package/bin/lib/init-runtime.mjs +46 -25
  12. package/bin/lib/init.mjs +3 -7
  13. package/bin/lib/lifecycle-preflight.mjs +333 -0
  14. package/bin/lib/lifecycle-state.mjs +476 -0
  15. package/bin/lib/manifest.mjs +24 -20
  16. package/bin/lib/phase.mjs +81 -26
  17. package/bin/lib/provenance.mjs +295 -54
  18. package/bin/lib/rendering.mjs +70 -12
  19. package/bin/lib/runtime-freshness.mjs +264 -0
  20. package/bin/lib/session-fingerprint.mjs +106 -0
  21. package/distilled/DESIGN.md +707 -13
  22. package/distilled/EVIDENCE-INDEX.md +166 -1
  23. package/distilled/README.md +44 -28
  24. package/distilled/SKILL.md +4 -4
  25. package/distilled/templates/agents.block.md +1 -1
  26. package/distilled/workflows/audit-milestone.md +50 -0
  27. package/distilled/workflows/complete-milestone.md +38 -2
  28. package/distilled/workflows/execute.md +16 -3
  29. package/distilled/workflows/map-codebase.md +15 -4
  30. package/distilled/workflows/new-milestone.md +53 -24
  31. package/distilled/workflows/new-project.md +44 -25
  32. package/distilled/workflows/pause.md +1 -1
  33. package/distilled/workflows/plan.md +187 -74
  34. package/distilled/workflows/progress.md +135 -31
  35. package/distilled/workflows/quick.md +20 -12
  36. package/distilled/workflows/resume.md +152 -65
  37. package/distilled/workflows/verify.md +55 -20
  38. package/docs/BROWNFIELD-PROOF.md +95 -0
  39. package/docs/RUNTIME-SUPPORT.md +77 -0
  40. package/docs/USER-GUIDE.md +443 -0
  41. package/docs/VERIFICATION-DISCIPLINE.md +59 -0
  42. package/docs/claude/context-monitor.md +98 -0
  43. package/docs/proof/consumer-node-cli/README.md +37 -0
  44. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -0
  45. package/docs/proof/consumer-node-cli/SPEC.md +17 -0
  46. package/docs/proof/consumer-node-cli/brief.md +9 -0
  47. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -0
  48. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -0
  49. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -0
  50. package/package.json +26 -20
@@ -9,7 +9,24 @@ function normalizeCount(value) {
9
9
  return Number.isFinite(numeric) ? numeric : 'unknown';
10
10
  }
11
11
 
12
- export function parseGitStatusShort(statusText = '') {
12
+ function normalizeCheckpointWorkflow(workflow) {
13
+ const normalized = String(workflow || 'generic').trim().toLowerCase();
14
+ return ['phase', 'quick', 'generic'].includes(normalized) ? normalized : 'generic';
15
+ }
16
+
17
+ export function classifyCheckpointRouting(workflow) {
18
+ const normalizedWorkflow = normalizeCheckpointWorkflow(workflow);
19
+ const progressBlocks = normalizedWorkflow === 'phase' || normalizedWorkflow === 'quick';
20
+
21
+ return {
22
+ workflow: normalizedWorkflow,
23
+ routingClass: progressBlocks ? 'blocking' : 'informational',
24
+ progressBlocks,
25
+ resumeOwnsCleanup: true,
26
+ };
27
+ }
28
+
29
+ export function parseGitStatusShort(statusText = '') {
13
30
  const lines = statusText
14
31
  .replace(/\r\n/g, '\n')
15
32
  .split('\n')
@@ -34,26 +51,161 @@ export function parseGitStatusShort(statusText = '') {
34
51
  });
35
52
  }
36
53
 
37
- return {
38
- files,
39
- stagedCount: files.filter((file) => file.staged).length,
40
- unstagedCount: files.filter((file) => file.unstaged).length,
41
- untrackedCount: files.filter((file) => file.untracked).length,
42
- dirty: files.length > 0,
43
- };
44
- }
45
-
46
- export function buildProvenanceSnapshot({
47
- checkpoint = {},
48
- planning = {},
49
- git = {},
50
- } = {}) {
51
- const status = parseGitStatusShort(git.statusShort || '');
52
- const commitsAheadOfMain = normalizeCount(git.commitsAheadOfMain);
53
- const commitsAheadOfRemote = normalizeCount(git.commitsAheadOfRemote);
54
- const prState = normalizePrState(git.prState);
55
-
56
- const warnings = [];
54
+ return {
55
+ files,
56
+ stagedCount: files.filter((file) => file.staged).length,
57
+ unstagedCount: files.filter((file) => file.unstaged).length,
58
+ untrackedCount: files.filter((file) => file.untracked).length,
59
+ dirty: files.length > 0,
60
+ };
61
+ }
62
+
63
+ export function classifyBrownfieldCheckpointPrecedence({
64
+ checkpoint = {},
65
+ planning = {},
66
+ quick = {},
67
+ brownfieldChange = {},
68
+ git = {},
69
+ status = parseGitStatusShort(git.statusShort || ''),
70
+ } = {}) {
71
+ const checkpointRouting = classifyCheckpointRouting(checkpoint.workflow);
72
+ if (!brownfieldChange?.exists) {
73
+ return {
74
+ primary: checkpointRouting.progressBlocks ? 'checkpoint' : 'none',
75
+ checkpointRouting,
76
+ branchAligned: false,
77
+ scopeAligned: false,
78
+ executionActive: false,
79
+ checkpointCanOverrideBrownfield: false,
80
+ strictMatchRequired: false,
81
+ };
82
+ }
83
+
84
+ if (checkpointRouting.workflow === 'generic') {
85
+ return {
86
+ primary: 'brownfield_change',
87
+ checkpointRouting,
88
+ branchAligned: false,
89
+ scopeAligned: false,
90
+ executionActive: false,
91
+ checkpointCanOverrideBrownfield: false,
92
+ strictMatchRequired: true,
93
+ };
94
+ }
95
+
96
+ const brownfieldMismatch = classifyBrownfieldArtifactMismatch({
97
+ brownfieldChange,
98
+ git,
99
+ status,
100
+ });
101
+ const currentBranch = normalizeBranchName(git.branch);
102
+ const checkpointBranch = normalizeBranchName(checkpoint.branch);
103
+ const brownfieldBranch = normalizeBranchName(brownfieldChange.currentIntegrationSurface);
104
+ const branchAligned = Boolean(
105
+ currentBranch
106
+ && checkpointBranch
107
+ && brownfieldBranch
108
+ && checkpointBranch === currentBranch
109
+ && brownfieldBranch === currentBranch
110
+ );
111
+ const scopeAligned = !brownfieldMismatch.warnings.some((warning) =>
112
+ warning.id === 'brownfield_scope_mismatch' || warning.id === 'brownfield_status_mismatch'
113
+ );
114
+ const executionActive = checkpointExecutionIsActive({
115
+ checkpoint,
116
+ checkpointRouting,
117
+ planning,
118
+ quick,
119
+ });
120
+ const checkpointCanOverrideBrownfield = branchAligned && scopeAligned && executionActive;
121
+
122
+ return {
123
+ primary: checkpointCanOverrideBrownfield ? 'checkpoint' : 'brownfield_change',
124
+ checkpointRouting,
125
+ branchAligned,
126
+ scopeAligned,
127
+ executionActive,
128
+ checkpointCanOverrideBrownfield,
129
+ strictMatchRequired: true,
130
+ };
131
+ }
132
+
133
+ export function classifyBrownfieldArtifactMismatch({
134
+ brownfieldChange = {},
135
+ git = {},
136
+ status = parseGitStatusShort(git.statusShort || ''),
137
+ } = {}) {
138
+ if (!brownfieldChange?.exists) {
139
+ return {
140
+ warnings: [],
141
+ requiresAcknowledgement: false,
142
+ outsideOwnedPaths: [],
143
+ };
144
+ }
145
+
146
+ const warnings = [];
147
+ const declaredBranch = normalizeDeclaredBranch(brownfieldChange.currentIntegrationSurface);
148
+ const branch = String(git.branch || '').trim().toLowerCase();
149
+ if (declaredBranch && branch && declaredBranch !== branch) {
150
+ warnings.push({
151
+ id: 'brownfield_branch_mismatch',
152
+ severity: 'acknowledgement_required',
153
+ summary: `CHANGE.md says the active integration surface is "${brownfieldChange.currentIntegrationSurface}", but git reports "${git.branch}".`,
154
+ });
155
+ }
156
+
157
+ const ownedPaths = normalizeOwnedPaths(brownfieldChange.declaredOwnedPaths || []);
158
+ const outsideOwnedPaths = ownedPaths.length === 0
159
+ ? []
160
+ : status.files
161
+ .map((file) => file.path)
162
+ .filter((filePath) => !ownedPaths.some((ownedPath) => matchesOwnedPath(filePath, ownedPath)));
163
+ if (outsideOwnedPaths.length > 0) {
164
+ warnings.push({
165
+ id: 'brownfield_scope_mismatch',
166
+ severity: 'acknowledgement_required',
167
+ summary: `Dirty files fall outside the CHANGE.md write scope: ${outsideOwnedPaths.join(', ')}`,
168
+ });
169
+ }
170
+
171
+ if ((brownfieldChange.currentStatus === 'closed' || brownfieldChange.currentStatus === 'ready_for_verification') && status.dirty) {
172
+ warnings.push({
173
+ id: 'brownfield_status_mismatch',
174
+ severity: 'acknowledgement_required',
175
+ summary: `CHANGE.md marks the change as "${brownfieldChange.currentStatus}", but the live worktree is still dirty.`,
176
+ });
177
+ }
178
+
179
+ return {
180
+ warnings,
181
+ requiresAcknowledgement: warnings.some((warning) => warning.severity === 'acknowledgement_required'),
182
+ outsideOwnedPaths,
183
+ };
184
+ }
185
+
186
+ export function buildProvenanceSnapshot({
187
+ checkpoint = {},
188
+ planning = {},
189
+ quick = {},
190
+ brownfieldChange = {},
191
+ git = {},
192
+ } = {}) {
193
+ const checkpointRouting = classifyCheckpointRouting(checkpoint.workflow);
194
+ const status = parseGitStatusShort(git.statusShort || '');
195
+ const commitsAheadOfMain = normalizeCount(git.commitsAheadOfMain);
196
+ const commitsAheadOfRemote = normalizeCount(git.commitsAheadOfRemote);
197
+ const prState = normalizePrState(git.prState);
198
+ const brownfieldMismatch = classifyBrownfieldArtifactMismatch({ brownfieldChange, git, status });
199
+ const brownfieldRouting = classifyBrownfieldCheckpointPrecedence({
200
+ checkpoint,
201
+ planning,
202
+ quick,
203
+ brownfieldChange,
204
+ git,
205
+ status,
206
+ });
207
+
208
+ const warnings = [];
57
209
 
58
210
  if (status.dirty) {
59
211
  warnings.push({
@@ -103,30 +255,50 @@ export function buildProvenanceSnapshot({
103
255
  });
104
256
  }
105
257
 
106
- if (git.materialCheckpointMismatch) {
107
- warnings.push({
108
- id: 'checkpoint_mismatch',
109
- severity: 'acknowledgement_required',
110
- summary: 'Checkpoint narrative truth materially understates or conflicts with the live branch/worktree truth.',
111
- });
112
- }
113
-
114
- return {
115
- checkpoint: {
116
- workflow: checkpoint.workflow || 'generic',
117
- phase: checkpoint.phase ?? null,
118
- runtime: checkpoint.runtime || 'unknown',
119
- hasNarrative: Boolean(checkpoint.hasNarrative),
120
- },
121
- planning: {
122
- currentPhase: planning.currentPhase || null,
123
- nextPhase: planning.nextPhase || null,
124
- completedPhaseCount: Number.isFinite(Number(planning.completedPhaseCount))
125
- ? Number(planning.completedPhaseCount)
126
- : 0,
127
- },
128
- git: {
129
- branch: git.branch || 'unknown',
258
+ if (git.materialCheckpointMismatch) {
259
+ warnings.push({
260
+ id: 'checkpoint_mismatch',
261
+ severity: 'acknowledgement_required',
262
+ summary: 'Checkpoint narrative truth materially understates or conflicts with the live branch/worktree truth.',
263
+ });
264
+ }
265
+
266
+ warnings.push(...brownfieldMismatch.warnings);
267
+
268
+ return {
269
+ checkpoint: {
270
+ workflow: checkpointRouting.workflow,
271
+ phase: checkpoint.phase ?? null,
272
+ branch: checkpoint.branch || null,
273
+ runtime: checkpoint.runtime || 'unknown',
274
+ hasNarrative: Boolean(checkpoint.hasNarrative),
275
+ routing: checkpointRouting,
276
+ },
277
+ planning: {
278
+ currentPhase: planning.currentPhase || null,
279
+ nextPhase: planning.nextPhase || null,
280
+ completedPhaseCount: Number.isFinite(Number(planning.completedPhaseCount))
281
+ ? Number(planning.completedPhaseCount)
282
+ : 0,
283
+ },
284
+ brownfieldChange: {
285
+ exists: Boolean(brownfieldChange?.exists),
286
+ title: brownfieldChange?.title || null,
287
+ currentStatus: brownfieldChange?.currentStatus || null,
288
+ currentIntegrationSurface: brownfieldChange?.currentIntegrationSurface || null,
289
+ nextAction: brownfieldChange?.nextAction || null,
290
+ declaredOwnedPaths: brownfieldChange?.declaredOwnedPaths || [],
291
+ },
292
+ routing: {
293
+ primary: brownfieldRouting.primary,
294
+ strictMatchRequired: brownfieldRouting.strictMatchRequired,
295
+ checkpointCanOverrideBrownfield: brownfieldRouting.checkpointCanOverrideBrownfield,
296
+ branchAligned: brownfieldRouting.branchAligned,
297
+ scopeAligned: brownfieldRouting.scopeAligned,
298
+ executionActive: brownfieldRouting.executionActive,
299
+ },
300
+ git: {
301
+ branch: git.branch || 'unknown',
130
302
  prState,
131
303
  commitsAheadOfMain,
132
304
  commitsAheadOfRemote,
@@ -135,12 +307,81 @@ export function buildProvenanceSnapshot({
135
307
  untrackedCount: status.untrackedCount,
136
308
  dirty: status.dirty,
137
309
  },
138
- integrationSurface: {
139
- staleBranch: Boolean(git.staleBranch),
140
- mixedScope: Boolean(git.mixedScope),
141
- materialCheckpointMismatch: Boolean(git.materialCheckpointMismatch),
142
- },
143
- warnings,
144
- requiresAcknowledgement: warnings.some((warning) => warning.severity === 'acknowledgement_required'),
145
- };
146
- }
310
+ integrationSurface: {
311
+ staleBranch: Boolean(git.staleBranch),
312
+ mixedScope: Boolean(git.mixedScope),
313
+ materialCheckpointMismatch: Boolean(git.materialCheckpointMismatch),
314
+ materialBrownfieldMismatch: brownfieldMismatch.requiresAcknowledgement,
315
+ },
316
+ warnings,
317
+ requiresAcknowledgement: warnings.some((warning) => warning.severity === 'acknowledgement_required'),
318
+ };
319
+ }
320
+
321
+ function normalizeBranchName(value) {
322
+ return String(value || '')
323
+ .trim()
324
+ .toLowerCase()
325
+ .replace(/\\/g, '/');
326
+ }
327
+
328
+ function normalizeDeclaredBranch(value) {
329
+ return normalizeBranchName(value);
330
+ }
331
+
332
+ function normalizeOwnedPaths(values) {
333
+ return values
334
+ .map((value) => String(value || '').trim().replace(/\\/g, '/'))
335
+ .filter(Boolean);
336
+ }
337
+
338
+ function matchesOwnedPath(filePath, ownedPath) {
339
+ if (ownedPath.includes('*')) {
340
+ const prefix = ownedPath.split('*')[0];
341
+ return filePath.startsWith(prefix);
342
+ }
343
+ return filePath === ownedPath
344
+ || filePath.startsWith(`${ownedPath}/`)
345
+ || ownedPath.startsWith(`${filePath}/`);
346
+ }
347
+
348
+ function checkpointExecutionIsActive({
349
+ checkpoint = {},
350
+ checkpointRouting = classifyCheckpointRouting(checkpoint.workflow),
351
+ planning = {},
352
+ quick = {},
353
+ } = {}) {
354
+ if (checkpointRouting.workflow === 'quick') {
355
+ return quick.hasIncompleteWork === true;
356
+ }
357
+
358
+ if (checkpointRouting.workflow !== 'phase') {
359
+ return false;
360
+ }
361
+
362
+ const checkpointPhase = normalizePhaseRef(checkpoint.phase);
363
+ if (!checkpointPhase) return false;
364
+
365
+ const phases = Array.isArray(planning.phases) ? planning.phases : [];
366
+ if (phases.length > 0) {
367
+ const matchingPhase = phases.find((phase) => normalizePhaseRef(phase.number) === checkpointPhase);
368
+ return Boolean(matchingPhase && matchingPhase.status !== 'done');
369
+ }
370
+
371
+ const currentPhase = normalizePhaseRef(planning.currentPhase);
372
+ const nextPhase = normalizePhaseRef(planning.nextPhase);
373
+ return checkpointPhase === currentPhase || checkpointPhase === nextPhase;
374
+ }
375
+
376
+ function normalizePhaseRef(value) {
377
+ const raw = String(value || '').trim().toLowerCase();
378
+ if (!raw) return '';
379
+
380
+ const match = raw.match(/^(\d+(?:\.\d+)*)([a-z]?)$/i);
381
+ if (!match) return raw;
382
+
383
+ const numericSegments = match[1]
384
+ .split('.')
385
+ .map((segment) => String(parseInt(segment, 10)));
386
+ return `${numericSegments.join('.')}${match[2] || ''}`;
387
+ }
@@ -18,16 +18,72 @@ function getDelegateContent(delegateFile) {
18
18
  return `<!-- Delegate file not found: ${delegateFile} -->\n`;
19
19
  }
20
20
 
21
- function renderSkillContent(workflow) {
22
- const workflowContent = getWorkflowContent(workflow.workflow);
23
- return `---
24
- name: ${workflow.name}
25
- description: ${workflow.description}
21
+ function renderSkillContent(workflow) {
22
+ const workflowContent = getWorkflowContent(workflow.workflow);
23
+ return `---
24
+ name: ${workflow.name}
25
+ description: ${workflow.description}
26
26
  context: fork
27
27
  agent: ${workflow.agent}
28
28
  ---
29
29
 
30
- ${workflowContent}`;
30
+ ${workflowContent}`;
31
+ }
32
+
33
+ function renderPlanningCliLauncher({ packageName = 'gsdd-cli', packageVersion }) {
34
+ if (!packageVersion) {
35
+ throw new Error('renderPlanningCliLauncher requires packageVersion');
36
+ }
37
+
38
+ const packageSpec = `${packageName}@${packageVersion}`;
39
+
40
+ return `#!/usr/bin/env node
41
+
42
+ import { spawnSync } from 'node:child_process';
43
+
44
+ const packageSpec = ${JSON.stringify(packageSpec)};
45
+ const args = process.argv.slice(2);
46
+ const env = { ...process.env };
47
+
48
+ function forwardResult(result, fallbackMessage) {
49
+ if (result.error) {
50
+ console.error(fallbackMessage ?? result.error.message);
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+
55
+ if (typeof result.status === 'number') {
56
+ process.exitCode = result.status;
57
+ return;
58
+ }
59
+
60
+ if (result.signal) {
61
+ process.exitCode = 1;
62
+ }
63
+ }
64
+
65
+ if (env.GSDD_CLI_PATH) {
66
+ const localResult = spawnSync(process.execPath, [env.GSDD_CLI_PATH, ...args], {
67
+ stdio: 'inherit',
68
+ env,
69
+ });
70
+ forwardResult(localResult, 'Failed to run the local GSDD CLI path from GSDD_CLI_PATH.');
71
+ } else {
72
+ const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
73
+ const npxResult = spawnSync(npxCommand, ['--yes', packageSpec, ...args], {
74
+ stdio: 'inherit',
75
+ env,
76
+ });
77
+ forwardResult(npxResult, \`Failed to run \${packageSpec} via npx.\`);
78
+ }
79
+ `;
80
+ }
81
+
82
+ function buildPortableSkillEntries(workflows) {
83
+ return workflows.map((workflow) => ({
84
+ relativePath: `.agents/skills/${workflow.name}/SKILL.md`,
85
+ content: renderSkillContent(workflow),
86
+ }));
31
87
  }
32
88
 
33
89
  function renderOpenCodeCommandContent(workflow) {
@@ -85,11 +141,13 @@ function upsertBoundedBlock(existing, blockContent) {
85
141
  }
86
142
 
87
143
  export {
144
+ buildPortableSkillEntries,
88
145
  getDelegateContent,
89
146
  getWorkflowContent,
90
- renderAgentsBoundedBlock,
91
- renderAgentsFileContent,
92
- renderOpenCodeCommandContent,
93
- renderSkillContent,
94
- upsertBoundedBlock,
95
- };
147
+ renderAgentsBoundedBlock,
148
+ renderAgentsFileContent,
149
+ renderOpenCodeCommandContent,
150
+ renderPlanningCliLauncher,
151
+ renderSkillContent,
152
+ upsertBoundedBlock,
153
+ };